From 30fe3773ae5bd430995057c8a22d375c61e6fe7c Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Thu, 19 May 2022 15:03:51 +0200 Subject: [PATCH 001/314] refactor - better naming, return native user id and not sip user id and create a dm with the native user instead of with the sip user --- .../features/call/dialpad/DialPadLookup.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index e835a74fd6..14ce5f2dc0 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -42,18 +42,23 @@ class DialPadLookup @Inject constructor( val sipUserId = thirdPartyUser.userId val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId) // If I have a native user I check for an existing native room with him... - val roomId = if (nativeLookupResults.isNotEmpty()) { + if (nativeLookupResults.isNotEmpty()) { val nativeUserId = nativeLookupResults.first().userId if (nativeUserId == session.myUserId) { throw Failure.NumberIsYours } - session.roomService().getExistingDirectRoomWithUser(nativeUserId) - // if there is not, just create a DM with the sip user - ?: directRoomHelper.ensureDMExists(sipUserId) - } else { - // do the same if there is no corresponding native user. - directRoomHelper.ensureDMExists(sipUserId) + var nativeRoomId = session.getExistingDirectRoomWithUser(nativeUserId) + if (nativeRoomId == null) { + // if there is no existing native room with the existing native user, + // just create a DM with the native user + nativeRoomId = directRoomHelper.ensureDMExists(nativeUserId) + } + Timber.d("lookupPhoneNumber with nativeUserId: $nativeUserId and nativeRoomId: $nativeRoomId") + return Result(userId = nativeUserId, roomId = nativeRoomId) } - return Result(userId = sipUserId, roomId = roomId) + // If there is no native user then we return sipUserId and sipRoomId - this is usually a PSTN call. + val sipRoomId = directRoomHelper.ensureDMExists(sipUserId) + Timber.d("lookupPhoneNumber with sipRoomId: $sipRoomId and sipUserId: $sipUserId") + return Result(userId = sipUserId, roomId = sipRoomId) } } From 8c783f94142eaa2e36c01aa6d4885370e8d49528 Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Thu, 19 May 2022 15:12:04 +0200 Subject: [PATCH 002/314] Create 6101.bugfix --- changelog.d/6101.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6101.bugfix diff --git a/changelog.d/6101.bugfix b/changelog.d/6101.bugfix new file mode 100644 index 0000000000..2d8da5327d --- /dev/null +++ b/changelog.d/6101.bugfix @@ -0,0 +1 @@ +Refactor - better naming, return native user id and not sip user id and create a dm with the native user instead of with the sip user. From f949c517b6f369d79664fc0160aba1646006c200 Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Fri, 20 May 2022 15:52:43 +0200 Subject: [PATCH 003/314] import timber and use .roomService() --- .../java/im/vector/app/features/call/dialpad/DialPadLookup.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 14ce5f2dc0..3ab2ee50c0 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -23,6 +23,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import org.matrix.android.sdk.api.session.Session import javax.inject.Inject +import timber.log.Timber class DialPadLookup @Inject constructor( private val session: Session, @@ -47,7 +48,7 @@ class DialPadLookup @Inject constructor( if (nativeUserId == session.myUserId) { throw Failure.NumberIsYours } - var nativeRoomId = session.getExistingDirectRoomWithUser(nativeUserId) + var nativeRoomId = session.roomService().getExistingDirectRoomWithUser(nativeUserId) if (nativeRoomId == null) { // if there is no existing native room with the existing native user, // just create a DM with the native user From b9b0e847044ae09018fa2417277faa3d76b99c35 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 23 May 2022 14:09:18 +0200 Subject: [PATCH 004/314] Adds set method to MutableDataSource --- .../main/java/im/vector/app/core/utils/DataSource.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index f83eda68e9..32c1cf9424 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -25,6 +25,9 @@ interface DataSource { } interface MutableDataSource : DataSource { + + suspend fun set(value: T) + fun post(value: T) } @@ -42,6 +45,10 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD return mutableFlow } + override suspend fun set(value: T) { + mutableFlow.emit(value) + } + override fun post(value: T) { mutableFlow.tryEmit(value) } @@ -58,6 +65,10 @@ open class PublishDataSource : MutableDataSource { return mutableFlow } + override suspend fun set(value: T) { + mutableFlow.emit(value) + } + override fun post(value: T) { mutableFlow.tryEmit(value) } From f831252e354c94fa47fc1480618b5822e1abd468 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 23 May 2022 14:09:29 +0200 Subject: [PATCH 005/314] Fixes UpgradeRoom command not working --- .../composer/MessageComposerViewModel.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index eca5c846ca..d0247553aa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -472,14 +472,16 @@ class MessageComposerViewModel @AssistedInject constructor( Unit } is ParsedCommand.UpgradeRoom -> { - _viewEvents.post( - MessageComposerViewEvents.ShowRoomUpgradeDialog( - parsedCommand.newVersion, - room.roomSummary()?.isPublic ?: false - ) - ) - _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + viewModelScope.launch { + _viewEvents.set( + MessageComposerViewEvents.ShowRoomUpgradeDialog( + parsedCommand.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) + _viewEvents.set(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } } } } From b0ce32e97ec17947d6f909bff9417427de2595fe Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Wed, 25 May 2022 15:37:31 +0200 Subject: [PATCH 006/314] Adds changelog file --- changelog.d/6154.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6154.bugfix diff --git a/changelog.d/6154.bugfix b/changelog.d/6154.bugfix new file mode 100644 index 0000000000..5c64eb2879 --- /dev/null +++ b/changelog.d/6154.bugfix @@ -0,0 +1 @@ +Fixed /upgraderoom command not doing anything From e84f012b735a58d72cddbacafe03145e70da14ae Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 26 May 2022 15:45:18 +0200 Subject: [PATCH 007/314] Changes set method name to emit --- vector/src/main/java/im/vector/app/core/utils/DataSource.kt | 6 +++--- .../home/room/detail/composer/MessageComposerViewModel.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 32c1cf9424..ff55b05689 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -26,7 +26,7 @@ interface DataSource { interface MutableDataSource : DataSource { - suspend fun set(value: T) + suspend fun emit(value: T) fun post(value: T) } @@ -45,7 +45,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD return mutableFlow } - override suspend fun set(value: T) { + override suspend fun emit(value: T) { mutableFlow.emit(value) } @@ -65,7 +65,7 @@ open class PublishDataSource : MutableDataSource { return mutableFlow } - override suspend fun set(value: T) { + override suspend fun emit(value: T) { mutableFlow.emit(value) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index d0247553aa..fcc4313558 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -473,13 +473,13 @@ class MessageComposerViewModel @AssistedInject constructor( } is ParsedCommand.UpgradeRoom -> { viewModelScope.launch { - _viewEvents.set( + _viewEvents.emit( MessageComposerViewEvents.ShowRoomUpgradeDialog( parsedCommand.newVersion, room.roomSummary()?.isPublic ?: false ) ) - _viewEvents.set(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + _viewEvents.emit(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } } From c48fd7708cf00c9c6cf88b0af062a66153107822 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 May 2022 20:11:54 +0200 Subject: [PATCH 008/314] Increase extraBufferCapacity (and allow configuration) and validate behavior with tests --- .../im/vector/app/core/utils/DataSource.kt | 7 +- .../composer/MessageComposerViewModel.kt | 18 ++-- .../vector/app/core/utils/DataSourceTest.kt | 82 +++++++++++++++++++ 3 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index ff55b05689..21316a7bc0 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -56,10 +56,13 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD /** * This datasource only emits all subsequent observed values to each subscriber. + * + * @param bufferSize number of buffered items before it starts dropping oldest. Should be at least 1 + * */ -open class PublishDataSource : MutableDataSource { +open class PublishDataSource(bufferSize: Int = 10) : MutableDataSource { - private val mutableFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val mutableFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = bufferSize, onBufferOverflow = BufferOverflow.DROP_OLDEST) override fun stream(): Flow { return mutableFlow diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index fcc4313558..eca5c846ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -472,16 +472,14 @@ class MessageComposerViewModel @AssistedInject constructor( Unit } is ParsedCommand.UpgradeRoom -> { - viewModelScope.launch { - _viewEvents.emit( - MessageComposerViewEvents.ShowRoomUpgradeDialog( - parsedCommand.newVersion, - room.roomSummary()?.isPublic ?: false - ) - ) - _viewEvents.emit(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() - } + _viewEvents.post( + MessageComposerViewEvents.ShowRoomUpgradeDialog( + parsedCommand.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() } } } diff --git a/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt new file mode 100644 index 0000000000..c9b351d95d --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt @@ -0,0 +1,82 @@ +/* + * 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.utils + +import im.vector.app.test.test +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.amshove.kluent.shouldContainSame +import org.junit.Test + +class DataSourceTest { + + @Test + fun `given PublishDataSource, when posting values before observing, then no value is observed`() = runTest { + val publishDataSource = PublishDataSource() + publishDataSource.post(0) + publishDataSource.post(1) + + publishDataSource.stream() + .test(this) + .assertNoValues() + .finish() + } + + @Test + fun `given PublishDataSource with a large enough buffer size, when posting values after observing, then only the latest values are observed`() = runTest { + val valuesToPost = listOf(2, 3, 4, 5, 6, 7, 8, 9) + val publishDataSource = PublishDataSource(bufferSize = valuesToPost.size) + publishDataSource.test(testScheduler, valuesToPost, valuesToPost) + } + + @Test + fun `given PublishDataSource with a too small buffer size, when posting values after observing, then we are missing some values`() = runTest { + val valuesToPost = listOf(2, 3, 4, 5, 6, 7, 8, 9) + val expectedValues = listOf(2, 9) + val publishDataSource = PublishDataSource(bufferSize = 1) + publishDataSource.test(testScheduler, valuesToPost, expectedValues) + + } + + private suspend fun PublishDataSource.test(testScheduler: TestCoroutineScheduler, valuesToPost: List, expectedValues: List) { + val values = ArrayList() + val job = stream() + .onEach { + // Artificial delay to make consumption longer than production + delay(10) + values.add(it) + } + .launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) + + valuesToPost.forEach { + post(it) + } + withContext(Dispatchers.Default) { + delay(11L * valuesToPost.size) + } + job.cancel() + + values shouldContainSame expectedValues + } +} From f3d7127f17a671ecb8f355dd696a34e7371d2714 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Fri, 3 Jun 2022 11:41:47 +0200 Subject: [PATCH 009/314] Fixes lint error --- vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt index c9b351d95d..f7b6d4a2c8 100644 --- a/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt +++ b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt @@ -56,7 +56,6 @@ class DataSourceTest { val expectedValues = listOf(2, 9) val publishDataSource = PublishDataSource(bufferSize = 1) publishDataSource.test(testScheduler, valuesToPost, expectedValues) - } private suspend fun PublishDataSource.test(testScheduler: TestCoroutineScheduler, valuesToPost: List, expectedValues: List) { From 9c1c87ba5d6c6f102bc5183bdbb04648dc41263d Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Fri, 3 Jun 2022 12:05:12 +0200 Subject: [PATCH 010/314] Fixes detekt error on java doc --- vector/src/main/java/im/vector/app/core/utils/DataSource.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 21316a7bc0..6deca9a6ea 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -57,8 +57,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD /** * This datasource only emits all subsequent observed values to each subscriber. * - * @param bufferSize number of buffered items before it starts dropping oldest. Should be at least 1 - * + * @property bufferSize number of buffered items before it starts dropping oldest. Should be at least 1 */ open class PublishDataSource(bufferSize: Int = 10) : MutableDataSource { From a5fd11c20446988e186b431e25d959d9e398ff3c Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Fri, 3 Jun 2022 12:50:05 +0200 Subject: [PATCH 011/314] Fixes detekt error --- vector/src/main/java/im/vector/app/core/utils/DataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 6deca9a6ea..f0880bb0f9 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -57,7 +57,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD /** * This datasource only emits all subsequent observed values to each subscriber. * - * @property bufferSize number of buffered items before it starts dropping oldest. Should be at least 1 + * bufferSize - number of buffered items before it starts dropping oldest. Should be at least 1 */ open class PublishDataSource(bufferSize: Int = 10) : MutableDataSource { From d586f64338231153db216a7eba9a516c4b7bdb53 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Fri, 3 Jun 2022 14:53:26 +0200 Subject: [PATCH 012/314] Removes emit method from DataSource --- .../main/java/im/vector/app/core/utils/DataSource.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index f0880bb0f9..60ad91272b 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -26,8 +26,6 @@ interface DataSource { interface MutableDataSource : DataSource { - suspend fun emit(value: T) - fun post(value: T) } @@ -45,10 +43,6 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD return mutableFlow } - override suspend fun emit(value: T) { - mutableFlow.emit(value) - } - override fun post(value: T) { mutableFlow.tryEmit(value) } @@ -67,10 +61,6 @@ open class PublishDataSource(bufferSize: Int = 10) : MutableDataSource { return mutableFlow } - override suspend fun emit(value: T) { - mutableFlow.emit(value) - } - override fun post(value: T) { mutableFlow.tryEmit(value) } From 31b245b8e36640e4ef6a472bf04a773f3223879c Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Wed, 8 Jun 2022 09:38:30 +0200 Subject: [PATCH 013/314] Changes test name --- vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt index f7b6d4a2c8..46c4406c8c 100644 --- a/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt +++ b/vector/src/test/java/im/vector/app/core/utils/DataSourceTest.kt @@ -44,7 +44,7 @@ class DataSourceTest { } @Test - fun `given PublishDataSource with a large enough buffer size, when posting values after observing, then only the latest values are observed`() = runTest { + fun `given PublishDataSource with a large enough buffer size, when posting values after observing, then all values are observed`() = runTest { val valuesToPost = listOf(2, 3, 4, 5, 6, 7, 8, 9) val publishDataSource = PublishDataSource(bufferSize = valuesToPost.size) publishDataSource.test(testScheduler, valuesToPost, valuesToPost) From ba18c6f3e278b02c95099d1588af53f4e28f82ff Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 17 May 2022 17:47:42 +0100 Subject: [PATCH 014/314] extracting registration action business logic to the handler abstraction and adding tests - renames the existing handler to a wizard delegate --- .../onboarding/OnboardingViewEvents.kt | 6 +- .../onboarding/OnboardingViewModel.kt | 100 ++++----- .../onboarding/RegistrationActionHandler.kt | 162 +++++++------- .../RegistrationWizardActionDelegate.kt | 123 +++++++++++ .../onboarding/ftueauth/FtueAuthVariant.kt | 50 +---- .../onboarding/OnboardingViewModelTest.kt | 71 ++---- .../RegistrationActionHandlerTest.kt | 202 +++++++++++------- .../RegistrationWizardActionDelegateTest.kt | 128 +++++++++++ ...er.kt => FakeRegistrationActionHandler.kt} | 16 +- .../FakeRegistrationWizardActionDelegate.kt | 39 ++++ .../app/test/fakes/FakeVectorFeatures.kt | 4 + .../SelectedHomeserverStateFixture.kt | 26 +++ 12 files changed, 599 insertions(+), 328 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt create mode 100644 vector/src/test/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegateTest.kt rename vector/src/test/java/im/vector/app/test/fakes/{FakeRegisterActionHandler.kt => FakeRegistrationActionHandler.kt} (65%) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizardActionDelegate.kt create mode 100644 vector/src/test/java/im/vector/app/test/fixtures/SelectedHomeserverStateFixture.kt diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index 5d6e7005c4..bf53a72cc3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -20,7 +20,7 @@ package im.vector.app.features.onboarding import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode -import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage /** * Transient events for Login. @@ -30,7 +30,9 @@ sealed class OnboardingViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : OnboardingViewEvents() data class DeeplinkAuthenticationFailure(val retryAction: OnboardingAction) : OnboardingViewEvents() - data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : OnboardingViewEvents() + object DisplayRegistrationFallback : OnboardingViewEvents() + data class DisplayRegistrationStage(val stage: Stage) : OnboardingViewEvents() + object DisplayStartRegistration : OnboardingViewEvents() object OutdatedHomeserver : OnboardingViewEvents() // Navigation event diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 61877a5f47..19f6d226ca 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -47,7 +47,6 @@ import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult -import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator import kotlinx.coroutines.Job import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -56,9 +55,7 @@ import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.login.LoginWizard -import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.RegistrationWizard -import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -80,11 +77,11 @@ class OnboardingViewModel @AssistedInject constructor( private val vectorFeatures: VectorFeatures, private val analyticsTracker: AnalyticsTracker, private val uriFilenameResolver: UriFilenameResolver, - private val registrationActionHandler: RegistrationActionHandler, private val directLoginUseCase: DirectLoginUseCase, private val startAuthenticationFlowUseCase: StartAuthenticationFlowUseCase, private val vectorOverrides: VectorOverrides, - private val buildMeta: BuildMeta + private val registrationActionHandler: RegistrationActionHandler, + private val buildMeta: BuildMeta, ) : VectorViewModel(initialState) { @AssistedFactory @@ -150,18 +147,18 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() - is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction, ::emitFlowResultViewEvent) - is OnboardingAction.ResetAction -> handleResetAction(action) - is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) - OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() - is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName) - OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete() - OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization() - OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile() - is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) - OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() - is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) - OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() + is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) + is OnboardingAction.ResetAction -> handleResetAction(action) + is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() + is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName) + OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete() + OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization() + OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile() + is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) + OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() + is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() } } @@ -259,12 +256,12 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { + private fun handleRegisterAction(action: RegisterAction) { val job = viewModelScope.launch { if (action.hasLoadingState()) { setState { copy(isLoading = true) } } - internalRegisterAction(action, onNextRegistrationStepAction) + internalRegisterAction(action) setState { copy(isLoading = false) } } @@ -275,23 +272,28 @@ class OnboardingViewModel @AssistedInject constructor( } } - private suspend fun internalRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { - runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) } + private suspend fun internalRegisterAction(action: RegisterAction, overrideNextStage: (() -> Unit)? = null) { + runCatching { registrationActionHandler.processAction(awaitState().selectedHomeserver, action) } .fold( onSuccess = { - when { - action.ignoresResult() -> { + when (it) { + RegistrationActionHandler.Result.Ignored -> { // do nothing } - else -> when (it) { - is RegistrationResult.Complete -> onSessionCreated( - it.session, - authenticationDescription = awaitState().selectedAuthenticationState.description - ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) - ) - is RegistrationResult.NextStep -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) - is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) - is RegistrationResult.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) + is RegistrationActionHandler.Result.NextStage -> { + overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayRegistrationStage(it.stage)) + } + is RegistrationActionHandler.Result.Success -> onSessionCreated( + it.session, + authenticationDescription = awaitState().selectedAuthenticationState.description + ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) + ) + RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) + RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) + is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) + RegistrationActionHandler.Result.MissingNextStage -> { + _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found"))) } } }, @@ -303,18 +305,6 @@ class OnboardingViewModel @AssistedInject constructor( ) } - private fun emitFlowResultViewEvent(flowResult: FlowResult) { - withState { state -> - val orderedResult = when { - state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy( - missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator()) - ) - else -> flowResult - } - _viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(orderedResult, isRegistrationStarted)) - } - } - private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl private fun handleRegisterWith(action: AuthenticateAction.Register) { @@ -328,8 +318,7 @@ class OnboardingViewModel @AssistedInject constructor( action.username, action.password, action.initialDeviceName - ), - ::emitFlowResultViewEvent + ) ) } @@ -382,8 +371,8 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) { updateSignMode(action.signMode) when (action.signMode) { - SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent) - SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration) + SignMode.SignIn -> startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.Unknown -> Unit } @@ -530,19 +519,6 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn)) } - private suspend fun onFlowResponse(flowResult: FlowResult, onNextRegistrationStepAction: (FlowResult) -> Unit) { - // If dummy stage is mandatory, and password is already sent, do the dummy stage now - if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { - handleRegisterDummy(onNextRegistrationStepAction) - } else { - onNextRegistrationStepAction(flowResult) - } - } - - private suspend fun handleRegisterDummy(onNextRegistrationStepAction: (FlowResult) -> Unit) { - internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction) - } - private suspend fun onSessionCreated(session: Session, authenticationDescription: AuthenticationDescription) { val state = awaitState() state.useCase?.let { useCase -> @@ -684,7 +660,7 @@ class OnboardingViewModel @AssistedInject constructor( } OnboardingFlow.SignUp -> { updateSignMode(SignMode.SignUp) - internalRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent) + internalRegisterAction(RegisterAction.StartRegistration) } OnboardingFlow.SignInSignUp, null -> { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index 3c3ac95cf2..9520413cd8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -16,105 +16,91 @@ package im.vector.app.features.onboarding +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.features.VectorFeatures +import im.vector.app.features.VectorOverrides +import im.vector.app.features.login.isSupported +import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator +import kotlinx.coroutines.flow.first +import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.registration.FlowResult -import org.matrix.android.sdk.api.auth.registration.RegisterThreePid -import org.matrix.android.sdk.api.auth.registration.RegistrationResult.FlowResponse -import org.matrix.android.sdk.api.auth.registration.RegistrationResult.Success -import org.matrix.android.sdk.api.auth.registration.RegistrationWizard -import org.matrix.android.sdk.api.failure.is401 +import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -import org.matrix.android.sdk.api.auth.registration.RegistrationResult as MatrixRegistrationResult -class RegistrationActionHandler @Inject constructor() { +class RegistrationActionHandler @Inject constructor( + private val registrationWizardActionDelegate: RegistrationWizardActionDelegate, + private val authenticationService: AuthenticationService, + private val vectorOverrides: VectorOverrides, + private val vectorFeatures: VectorFeatures, + stringProvider: StringProvider +) { - suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { - return when (action) { - RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } - is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } - is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } - is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } - is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) - is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } - is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } - is RegisterAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailIsValidated(registrationWizard, action.delayMillis) - is RegisterAction.CreateAccount -> resultOf { - registrationWizard.createAccount( - action.username, - action.password, - action.initialDeviceName - ) + private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + + suspend fun processAction(state: SelectedHomeserverState, action: RegisterAction): Result { + val result = registrationWizardActionDelegate.executeAction(action) + return when { + action.ignoresResult() -> Result.Ignored + else -> when (result) { + is RegistrationResult.Complete -> Result.Success(result.session) + is RegistrationResult.NextStep -> processFlowResult(result, state) + is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email) + is RegistrationResult.Error -> Result.Error(result.cause) } } } - private suspend fun handleAddThreePid(wizard: RegistrationWizard, action: RegisterAction.AddThreePid): RegistrationResult { - return runCatching { wizard.addThreePid(action.threePid) }.fold( - onSuccess = { it.toRegistrationResult() }, - onFailure = { - when { - action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) - else -> RegistrationResult.Error(it) - } - } - ) + private suspend fun processFlowResult(result: RegistrationResult.NextStep, state: SelectedHomeserverState): Result { + // If dummy stage is mandatory, and password is already sent, do the dummy stage now + return if (authenticationService.isRegistrationStarted() && result.flowResult.missingStages.hasMandatoryDummy()) { + processAction(state, RegisterAction.RegisterDummy) + } else { + handleNextStep(state, result.flowResult) + } } - private tailrec suspend fun handleCheckIfEmailIsValidated(registrationWizard: RegistrationWizard, delayMillis: Long): RegistrationResult { - return runCatching { registrationWizard.checkIfEmailHasBeenValidated(delayMillis) }.fold( - onSuccess = { it.toRegistrationResult() }, - onFailure = { - when { - it.is401() -> null // recursively continue to check with a delay - else -> RegistrationResult.Error(it) - } - } - ) ?: handleCheckIfEmailIsValidated(registrationWizard, 10_000) + private suspend fun handleNextStep(state: SelectedHomeserverState, flowResult: FlowResult): Result { + return when { + flowResult.registrationShouldFallback() -> Result.UnsupportedStage + authenticationService.isRegistrationStarted() -> findNextStage(state, flowResult) + else -> Result.StartRegistration + } + } + + private fun findNextStage(state: SelectedHomeserverState, flowResult: FlowResult): Result { + val orderedResult = when { + state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy( + missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator()) + ) + else -> flowResult + } + return orderedResult.findNextRegistrationStage() + ?.let { Result.NextStage(it) } + ?: Result.MissingNextStage + } + + private fun FlowResult.findNextRegistrationStage() = missingStages.firstMandatoryOrNull() ?: missingStages.ignoreDummy().firstOptionalOrNull() + + private suspend fun FlowResult.registrationShouldFallback() = vectorOverrides.forceLoginFallback.first() || missingStages.any { !it.isSupported() } + + private fun SelectedHomeserverState.hasSelectedMatrixOrg() = userFacingUrl == matrixOrgUrl + + sealed interface Result { + data class Success(val session: Session) : Result + data class NextStage(val stage: Stage) : Result + data class Error(val cause: Throwable) : Result + data class SendEmailSuccess(val email: String) : Result + object MissingNextStage : Result + object StartRegistration : Result + object UnsupportedStage : Result + object Ignored : Result } } -private inline fun resultOf(block: () -> MatrixRegistrationResult): RegistrationResult { - return runCatching { block() }.fold( - onSuccess = { it.toRegistrationResult() }, - onFailure = { RegistrationResult.Error(it) } - ) -} - -private fun MatrixRegistrationResult.toRegistrationResult() = when (this) { - is FlowResponse -> RegistrationResult.NextStep(flowResult) - is Success -> RegistrationResult.Complete(session) -} - -sealed interface RegistrationResult { - data class Error(val cause: Throwable) : RegistrationResult - data class Complete(val session: Session) : RegistrationResult - data class NextStep(val flowResult: FlowResult) : RegistrationResult - data class SendEmailSuccess(val email: String) : RegistrationResult -} - -sealed interface RegisterAction { - object StartRegistration : RegisterAction - data class CreateAccount(val username: String, val password: String, val initialDeviceName: String) : RegisterAction - - data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction - object SendAgainThreePid : RegisterAction - - // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) - data class ValidateThreePid(val code: String) : RegisterAction - - data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction - - data class CaptchaDone(val captchaResponse: String) : RegisterAction - object AcceptTerms : RegisterAction - object RegisterDummy : RegisterAction -} - -fun RegisterAction.ignoresResult() = when (this) { - is RegisterAction.SendAgainThreePid -> true - else -> false -} - -fun RegisterAction.hasLoadingState() = when (this) { - is RegisterAction.CheckIfEmailHasBeenValidated -> false - else -> true -} +private fun List.firstMandatoryOrNull() = firstOrNull { it.mandatory } +private fun List.firstOptionalOrNull() = firstOrNull { !it.mandatory } +private fun List.ignoreDummy() = filter { it !is Stage.Dummy } +private fun List.hasMandatoryDummy() = any { it is Stage.Dummy && it.mandatory } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt new file mode 100644 index 0000000000..5ce8bb857b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt @@ -0,0 +1,123 @@ +/* + * 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.onboarding + +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.FlowResponse +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.Success +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.failure.is401 +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject +import org.matrix.android.sdk.api.auth.registration.RegistrationResult as MatrixRegistrationResult + +class RegistrationWizardActionDelegate @Inject constructor( + private val authenticationService: AuthenticationService +) { + + private val registrationWizard: RegistrationWizard + get() = authenticationService.getRegistrationWizard() + + suspend fun executeAction(action: RegisterAction): RegistrationResult { + return when (action) { + RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } + is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } + is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } + is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } + is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) + is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } + is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } + is RegisterAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailIsValidated(registrationWizard, action.delayMillis) + is RegisterAction.CreateAccount -> resultOf { + registrationWizard.createAccount( + action.username, + action.password, + action.initialDeviceName + ) + } + } + } + + private suspend fun handleAddThreePid(wizard: RegistrationWizard, action: RegisterAction.AddThreePid): RegistrationResult { + return runCatching { wizard.addThreePid(action.threePid) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) + else -> RegistrationResult.Error(it) + } + } + ) + } + + private tailrec suspend fun handleCheckIfEmailIsValidated(registrationWizard: RegistrationWizard, delayMillis: Long): RegistrationResult { + return runCatching { registrationWizard.checkIfEmailHasBeenValidated(delayMillis) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + it.is401() -> null // recursively continue to check with a delay + else -> RegistrationResult.Error(it) + } + } + ) ?: handleCheckIfEmailIsValidated(registrationWizard, 10_000) + } +} + +private inline fun resultOf(block: () -> MatrixRegistrationResult): RegistrationResult { + return runCatching { block() }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { RegistrationResult.Error(it) } + ) +} + +private fun MatrixRegistrationResult.toRegistrationResult() = when (this) { + is FlowResponse -> RegistrationResult.NextStep(flowResult) + is Success -> RegistrationResult.Complete(session) +} + +sealed interface RegistrationResult { + data class Error(val cause: Throwable) : RegistrationResult + data class Complete(val session: Session) : RegistrationResult + data class NextStep(val flowResult: FlowResult) : RegistrationResult + data class SendEmailSuccess(val email: String) : RegistrationResult +} + +sealed interface RegisterAction { + object StartRegistration : RegisterAction + data class CreateAccount(val username: String, val password: String, val initialDeviceName: String) : RegisterAction + + data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction + object SendAgainThreePid : RegisterAction + data class ValidateThreePid(val code: String) : RegisterAction + data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction + + data class CaptchaDone(val captchaResponse: String) : RegisterAction + object AcceptTerms : RegisterAction + object RegisterDummy : RegisterAction +} + +fun RegisterAction.ignoresResult() = when (this) { + is RegisterAction.SendAgainThreePid -> true + else -> false +} + +fun RegisterAction.hasLoadingState() = when (this) { + is RegisterAction.CheckIfEmailHasBeenValidated -> false + else -> true +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index f8ad700b40..89e28740a4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -44,7 +44,6 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode import im.vector.app.features.login.TextInputFormFragmentMode -import im.vector.app.features.login.isSupported import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.features.onboarding.OnboardingVariant @@ -129,10 +128,7 @@ class FtueAuthVariant( private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) { when (viewEvents) { - is OnboardingViewEvents.RegistrationFlowResult -> { - onRegistrationFlow(viewEvents) - } - is OnboardingViewEvents.OutdatedHomeserver -> { + is OnboardingViewEvents.OutdatedHomeserver -> { MaterialAlertDialogBuilder(activity) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) @@ -227,9 +223,15 @@ class FtueAuthVariant( option = commonOption ) } - OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() - OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() - is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents) + OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() + OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() + is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents) + OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog() + is OnboardingViewEvents.DisplayRegistrationStage -> doStage(viewEvents.stage) + OnboardingViewEvents.DisplayStartRegistration -> when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() + else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) + } } } @@ -253,25 +255,10 @@ class FtueAuthVariant( addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java) } - private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) { - when { - registrationShouldFallback(viewEvents) -> displayFallbackWebDialog() - viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.missingStages) - vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() - else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) - } - } - private fun openStartCombinedRegister() { addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java) } - private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) = - isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow() - - private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() = - flowResult.missingStages.any { !it.isSupported() } - private fun displayFallbackWebDialog() { MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_name) @@ -381,23 +368,6 @@ class FtueAuthVariant( ?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) } } - private fun handleRegistrationNavigation(remainingStages: List) { - // Complete all mandatory stages first - val mandatoryStage = remainingStages.firstOrNull { it.mandatory } - - if (mandatoryStage != null) { - doStage(mandatoryStage) - } else { - // Consider optional stages - val optionalStage = remainingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } - if (optionalStage == null) { - // Should not happen... - } else { - doStage(optionalStage) - } - } - } - private fun doStage(stage: Stage) { // Ensure there is no fragment for registration stage in the backstack supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 77539da232..ce32dfeb3d 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -31,9 +31,8 @@ import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeDirectLoginUseCase import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerHistoryService +import im.vector.app.test.fakes.FakeRegistrationActionHandler import im.vector.app.test.fakes.FakeLoginWizard -import im.vector.app.test.fakes.FakeRegisterActionHandler -import im.vector.app.test.fakes.FakeRegistrationWizard import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStartAuthenticationFlowUseCase import im.vector.app.test.fakes.FakeStringProvider @@ -50,7 +49,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities @@ -62,8 +60,7 @@ private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L) private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true) -private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList()) -private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT) +private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationActionHandler.Result.NextStage(Stage.Dummy(mandatory = true)) private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name") private const val A_HOMESERVER_URL = "https://edited-homeserver.org" private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) @@ -82,7 +79,7 @@ class OnboardingViewModelTest { private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession) private val fakeAuthenticationService = FakeAuthenticationService() - private val fakeRegisterActionHandler = FakeRegisterActionHandler() + private val fakeRegistrationActionHandler = FakeRegistrationActionHandler() private val fakeDirectLoginUseCase = FakeDirectLoginUseCase() private val fakeVectorFeatures = FakeVectorFeatures() private val fakeHomeServerConnectionConfigFactory = FakeHomeServerConnectionConfigFactory() @@ -199,7 +196,7 @@ class OnboardingViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true)) + .assertEvents(OnboardingViewEvents.DisplayRegistrationStage(ANY_CONTINUING_REGISTRATION_RESULT.stage)) .finish() } @@ -216,7 +213,7 @@ class OnboardingViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true)) + .assertEvents(OnboardingViewEvents.DisplayRegistrationStage(ANY_CONTINUING_REGISTRATION_RESULT.stage)) .finish() } @@ -229,14 +226,14 @@ class OnboardingViewModelTest { test .assertState(initialState) - .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true)) + .assertEvents(OnboardingViewEvents.DisplayRegistrationStage(ANY_CONTINUING_REGISTRATION_RESULT.stage)) .finish() } @Test fun `given register action ignores result, when handling action, then does nothing on success`() = runTest { val test = viewModel.test() - givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT)) + givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationActionHandler.Result.Ignored) viewModel.handle(OnboardingAction.PostRegisterAction(A_RESULT_IGNORED_REGISTER_ACTION)) @@ -276,7 +273,7 @@ class OnboardingViewModelTest { viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG) fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE)) - givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT)) + givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) val test = viewModel.test() @@ -318,11 +315,11 @@ class OnboardingViewModelTest { @Test fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest { fakeVectorFeatures.givenPersonalisationEnabled() - givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Complete(fakeSession)) givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) + givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Success(fakeSession)) val test = viewModel.test() - viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.StartRegistration)) test .assertStatesChanges( @@ -334,26 +331,6 @@ class OnboardingViewModelTest { .finish() } - @Test - fun `given personalisation enabled and registration has started and has dummy step to do, when handling action, then ignores other steps and does dummy`() { - runTest { - fakeVectorFeatures.givenPersonalisationEnabled() - givenSuccessfulRegistrationForStartAndDummySteps(missingStages = listOf(Stage.Dummy(mandatory = true))) - val test = viewModel.test() - - viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) - - test - .assertStatesChanges( - initialState, - { copy(isLoading = true) }, - { copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) } - ) - .assertEvents(OnboardingViewEvents.OnAccountCreated) - .finish() - } - } - @Test fun `given changing profile avatar is supported, when updating display name, then updates upstream user display name and moves to choose profile avatar`() { runTest { @@ -520,11 +497,11 @@ class OnboardingViewModelTest { fakeVectorFeatures, FakeAnalyticsTracker(), fakeUriFilenameResolver.instance, - fakeRegisterActionHandler.instance, fakeDirectLoginUseCase.instance, fakeStartAuthenticationFlowUseCase.instance, FakeVectorOverrides(), - aBuildMeta() + fakeRegistrationActionHandler.instance, + aBuildMeta(), ).also { viewModel = it initialState = state @@ -556,17 +533,6 @@ class OnboardingViewModelTest { ) } - private fun givenSuccessfulRegistrationForStartAndDummySteps(missingStages: List) { - val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList()) - givenRegistrationResultsFor( - listOf( - A_LOADABLE_REGISTER_ACTION to RegistrationResult.NextStep(flowResult), - RegisterAction.RegisterDummy to RegistrationResult.Complete(fakeSession) - ) - ) - givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) - } - private fun givenSuccessfullyCreatesAccount(homeServerCapabilities: HomeServerCapabilities) { fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities) givenInitialisesSession(fakeSession) @@ -578,21 +544,16 @@ class OnboardingViewModelTest { fakeSession.expectStartsSyncing() } - private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationResult) { + private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationActionHandler.Result) { givenRegistrationResultsFor(listOf(action to result)) } - private fun givenRegistrationResultsFor(results: List>) { - fakeAuthenticationService.givenRegistrationStarted(true) - val registrationWizard = FakeRegistrationWizard() - fakeAuthenticationService.givenRegistrationWizard(registrationWizard) - fakeRegisterActionHandler.givenResultsFor(registrationWizard, results) + private fun givenRegistrationResultsFor(results: List>) { + fakeRegistrationActionHandler.givenResultsFor(results) } private fun givenRegistrationActionErrors(action: RegisterAction, cause: Throwable) { - val registrationWizard = FakeRegistrationWizard() - fakeAuthenticationService.givenRegistrationWizard(registrationWizard) - fakeRegisterActionHandler.givenThrowsFor(registrationWizard, action, cause) + fakeRegistrationActionHandler.givenThrows(action, cause) } } diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt index f6d9317038..ffb1911b20 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt @@ -16,108 +16,166 @@ package im.vector.app.features.onboarding -import im.vector.app.test.fakes.FakeRegistrationWizard +import im.vector.app.R +import im.vector.app.test.fixtures.SelectedHomeserverStateFixture.aSelectedHomeserverState +import im.vector.app.test.fakes.FakeAuthenticationService +import im.vector.app.test.fakes.FakeRegistrationWizardActionDelegate import im.vector.app.test.fakes.FakeSession -import im.vector.app.test.fixtures.a401ServerError -import io.mockk.coVerifyAll +import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.fakes.FakeVectorFeatures +import im.vector.app.test.fakes.FakeVectorOverrides import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.RegisterThreePid -import org.matrix.android.sdk.api.auth.registration.RegistrationWizard -import org.matrix.android.sdk.api.auth.registration.RegistrationResult as SdkResult +import org.matrix.android.sdk.api.auth.registration.Stage -private const val IGNORED_DELAY = 0L -private val AN_ERROR = RuntimeException() private val A_SESSION = FakeSession() -private val AN_EXPECTED_RESULT = RegistrationResult.Complete(A_SESSION) -private const val A_USERNAME = "a username" -private const val A_PASSWORD = "a password" -private const val AN_INITIAL_DEVICE_NAME = "a device name" -private const val A_CAPTCHA_RESPONSE = "a captcha response" -private const val A_PID_CODE = "a pid code" -private const val EMAIL_VALIDATED_DELAY = 10000L -private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email") class RegistrationActionHandlerTest { - private val fakeRegistrationWizard = FakeRegistrationWizard() - private val registrationActionHandler = RegistrationActionHandler() + private val fakeWizardActionDelegate = FakeRegistrationWizardActionDelegate() + private val fakeAuthenticationService = FakeAuthenticationService() + private val vectorOverrides = FakeVectorOverrides() + private val vectorFeatures = FakeVectorFeatures() + private val fakeStringProvider = FakeStringProvider().also { + it.given(R.string.matrix_org_server_url, "https://matrix.org") + } + + private val registrationActionHandler = RegistrationActionHandler( + fakeWizardActionDelegate.instance, + fakeAuthenticationService, + vectorOverrides, + vectorFeatures, + fakeStringProvider.instance + ) @Test - fun `when handling register action then delegates to wizard`() = runTest { - val cases = listOf( - case(RegisterAction.StartRegistration) { getRegistrationFlow() }, - case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) }, - case(RegisterAction.AcceptTerms) { acceptTerms() }, - case(RegisterAction.RegisterDummy) { dummy() }, - case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) }, - case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() }, - case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) }, - case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) }, - case(RegisterAction.CreateAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)) { - createAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME) - } - ) + fun `when processing SendAgainThreePid, then ignores result`() = runTest { + val sendAgainThreePid = RegisterAction.SendAgainThreePid + fakeWizardActionDelegate.givenResultsFor(listOf(sendAgainThreePid to RegistrationResult.Complete(A_SESSION))) - cases.forEach { testSuccessfulActionDelegation(it) } + val result = registrationActionHandler.processAction(sendAgainThreePid) + + result shouldBeEqualTo RegistrationActionHandler.Result.Ignored } @Test - fun `given adding an email ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest { - fakeRegistrationWizard.givenAddEmailThreePidErrors( - cause = a401ServerError(), - email = A_PID_TO_REGISTER.email - ) + fun `given wizard delegate returns success, when handling action, then returns success`() = runTest { + fakeWizardActionDelegate.givenResultsFor(listOf(RegisterAction.StartRegistration to RegistrationResult.Complete(A_SESSION))) - val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.AddThreePid(A_PID_TO_REGISTER)) + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) - result shouldBeEqualTo RegistrationResult.SendEmailSuccess(A_PID_TO_REGISTER.email) + result shouldBeEqualTo RegistrationActionHandler.Result.Success(A_SESSION) } @Test - fun `given email verification errors with 401 then fatal error, when checking email validation, then continues to poll until non 401 error`() = runTest { - val errorsToThrow = listOf( - a401ServerError(), - a401ServerError(), - a401ServerError(), - AN_ERROR - ) - fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow) + fun `given flow result contains unsupported stages, when handling action, then returns UnsupportedStage`() = runTest { + fakeAuthenticationService.givenRegistrationStarted(false) + fakeWizardActionDelegate.givenResultsFor(listOf(RegisterAction.StartRegistration to anUnsupportedResult())) - val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY)) + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) - fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size) - result shouldBeEqualTo RegistrationResult.Error(AN_ERROR) + result shouldBeEqualTo RegistrationActionHandler.Result.UnsupportedStage } @Test - fun `given email verification errors with 401 and succeeds, when checking email validation, then continues to poll until success`() = runTest { - val errorsToThrow = listOf( - a401ServerError(), - a401ServerError(), - a401ServerError() + fun `given flow result with mandatory and optional stages, when handling action, then returns mandatory stage`() = runTest { + val mandatoryStage = Stage.ReCaptcha(mandatory = true, "ignored-key") + val mixedStages = listOf(Stage.Email(mandatory = false), mandatoryStage) + givenFlowResult(mixedStages) + + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.NextStage(mandatoryStage) + } + + @Test + fun `given flow result with only optional stages, when handling action, then returns optional stage`() = runTest { + val optionalStage = Stage.ReCaptcha(mandatory = false, "ignored-key") + givenFlowResult(listOf(optionalStage)) + + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.NextStage(optionalStage) + } + + @Test + fun `given flow result with missing stages, when handling action, then returns MissingNextStage`() = runTest { + givenFlowResult(emptyList()) + + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.MissingNextStage + } + + @Test + fun `given flow result with only optional dummy stage, when handling action, then returns MissingNextStage`() = runTest { + givenFlowResult(listOf(Stage.Dummy(mandatory = false))) + + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.MissingNextStage + } + + @Test + fun `given non matrix org homeserver and flow result with missing mandatory stages, when handling action, then returns first item`() = runTest { + val firstStage = Stage.ReCaptcha(mandatory = true, "ignored-key") + val orderedStages = listOf(firstStage, Stage.Email(mandatory = true), Stage.Msisdn(mandatory = true)) + givenFlowResult(orderedStages) + + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.NextStage(firstStage) + } + + @Test + fun `given matrix org homeserver and flow result with missing mandatory stages, when handling action, then returns email item first`() = runTest { + vectorFeatures.givenCombinedRegisterEnabled() + val expectedFirstItem = Stage.Email(mandatory = true) + val orderedStages = listOf(Stage.ReCaptcha(mandatory = true, "ignored-key"), expectedFirstItem, Stage.Msisdn(mandatory = true)) + givenFlowResult(orderedStages) + + val result = registrationActionHandler.processAction(state = aSelectedHomeserverState("https://matrix.org/"), RegisterAction.StartRegistration) + + result shouldBeEqualTo RegistrationActionHandler.Result.NextStage(expectedFirstItem) + } + + @Test + fun `given password already sent and missing mandatory dummy stage, when handling action, then fast tracks the dummy stage`() = runTest { + val stages = listOf(Stage.ReCaptcha(mandatory = true, "ignored-key"), Stage.Email(mandatory = true), Stage.Dummy(mandatory = true)) + fakeAuthenticationService.givenRegistrationStarted(true) + fakeWizardActionDelegate.givenResultsFor( + listOf( + RegisterAction.StartRegistration to aFlowResult(stages), + RegisterAction.RegisterDummy to RegistrationResult.Complete(A_SESSION) + ) ) - fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow, finally = SdkResult.Success(A_SESSION)) - val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY)) + val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) - fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size + 1) - result shouldBeEqualTo RegistrationResult.Complete(A_SESSION) + result shouldBeEqualTo RegistrationActionHandler.Result.Success(A_SESSION) } - private suspend fun testSuccessfulActionDelegation(case: Case) { - val fakeRegistrationWizard = FakeRegistrationWizard() - val registrationActionHandler = RegistrationActionHandler() - fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect) - - val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action) - - coVerifyAll { case.expect(fakeRegistrationWizard) } - result shouldBeEqualTo AN_EXPECTED_RESULT + private fun givenFlowResult(stages: List) { + fakeAuthenticationService.givenRegistrationStarted(true) + fakeWizardActionDelegate.givenResultsFor(listOf(RegisterAction.StartRegistration to aFlowResult(stages))) } + + private fun aFlowResult(missingStages: List) = RegistrationResult.NextStep( + FlowResult( + missingStages = missingStages, + completedStages = emptyList() + ) + ) + + private fun anUnsupportedResult() = RegistrationResult.NextStep( + FlowResult( + missingStages = listOf(Stage.Other(mandatory = true, "ignored-type", emptyMap())), + completedStages = emptyList() + ) + ) + + private suspend fun RegistrationActionHandler.processAction(action: RegisterAction) = processAction(aSelectedHomeserverState(), action) } - -private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> SdkResult) = Case(action, expect) - -private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> SdkResult) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegateTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegateTest.kt new file mode 100644 index 0000000000..a610486670 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegateTest.kt @@ -0,0 +1,128 @@ +/* + * 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.onboarding + +import im.vector.app.test.fakes.FakeAuthenticationService +import im.vector.app.test.fakes.FakeRegistrationWizard +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fixtures.a401ServerError +import io.mockk.coVerifyAll +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationResult as MatrixRegistrationResult + +private const val IGNORED_DELAY = 0L +private val AN_ERROR = RuntimeException() +private val A_SESSION = FakeSession() +private val AN_EXPECTED_RESULT = RegistrationResult.Complete(A_SESSION) +private const val A_USERNAME = "a username" +private const val A_PASSWORD = "a password" +private const val AN_INITIAL_DEVICE_NAME = "a device name" +private const val A_CAPTCHA_RESPONSE = "a captcha response" +private const val A_PID_CODE = "a pid code" +private const val EMAIL_VALIDATED_DELAY = 10000L +private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email") + +class RegistrationWizardActionDelegateTest { + + private val fakeRegistrationWizard = FakeRegistrationWizard() + private val fakeAuthenticationService = FakeAuthenticationService().also { + it.givenRegistrationWizard(fakeRegistrationWizard) + } + private val registrationActionHandler = RegistrationWizardActionDelegate(fakeAuthenticationService) + + @Test + fun `when handling register action then delegates to wizard`() = runTest { + val cases = listOf( + case(RegisterAction.StartRegistration) { getRegistrationFlow() }, + case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) }, + case(RegisterAction.AcceptTerms) { acceptTerms() }, + case(RegisterAction.RegisterDummy) { dummy() }, + case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) }, + case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() }, + case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) }, + case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) }, + case(RegisterAction.CreateAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)) { + createAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME) + } + ) + + cases.forEach { testSuccessfulActionDelegation(it) } + } + + @Test + fun `given adding an email ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest { + fakeRegistrationWizard.givenAddEmailThreePidErrors( + cause = a401ServerError(), + email = A_PID_TO_REGISTER.email + ) + + val result = registrationActionHandler.executeAction(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) + + result shouldBeEqualTo RegistrationResult.SendEmailSuccess(A_PID_TO_REGISTER.email) + } + + @Test + fun `given email verification errors with 401 then fatal error, when checking email validation, then continues to poll until non 401 error`() = runTest { + val errorsToThrow = listOf( + a401ServerError(), + a401ServerError(), + a401ServerError(), + AN_ERROR + ) + fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow) + + val result = registrationActionHandler.executeAction(RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY)) + + fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size) + result shouldBeEqualTo RegistrationResult.Error(AN_ERROR) + } + + @Test + fun `given email verification errors with 401 and succeeds, when checking email validation, then continues to poll until success`() = runTest { + val errorsToThrow = listOf( + a401ServerError(), + a401ServerError(), + a401ServerError() + ) + fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow, finally = MatrixRegistrationResult.Success(A_SESSION)) + + val result = registrationActionHandler.executeAction(RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY)) + + fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size + 1) + result shouldBeEqualTo RegistrationResult.Complete(A_SESSION) + } + + private suspend fun testSuccessfulActionDelegation(case: Case) { + val fakeRegistrationWizard = FakeRegistrationWizard() + val authenticationService = FakeAuthenticationService().also { it.givenRegistrationWizard(fakeRegistrationWizard) } + val registrationActionHandler = RegistrationWizardActionDelegate(authenticationService) + fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect) + + val result = registrationActionHandler.executeAction(case.action) + + coVerifyAll { case.expect(fakeRegistrationWizard) } + result shouldBeEqualTo AN_EXPECTED_RESULT + } +} + +private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> MatrixRegistrationResult) = Case(action, expect) + +private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> MatrixRegistrationResult) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationActionHandler.kt similarity index 65% rename from vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationActionHandler.kt index f5824e5866..ddaea38302 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationActionHandler.kt @@ -18,23 +18,21 @@ package im.vector.app.test.fakes import im.vector.app.features.onboarding.RegisterAction import im.vector.app.features.onboarding.RegistrationActionHandler -import im.vector.app.features.onboarding.RegistrationResult import io.mockk.coEvery import io.mockk.mockk -import org.matrix.android.sdk.api.auth.registration.RegistrationWizard -class FakeRegisterActionHandler { +class FakeRegistrationActionHandler { val instance = mockk() - fun givenResultsFor(wizard: RegistrationWizard, result: List>) { - coEvery { instance.handleRegisterAction(wizard, any()) } answers { call -> + fun givenThrows(action: RegisterAction, cause: Throwable) { + coEvery { instance.processAction(any(), action) } throws cause + } + + fun givenResultsFor(result: List>) { + coEvery { instance.processAction(any(), any()) } answers { call -> val actionArg = call.invocation.args[1] as RegisterAction result.first { it.first == actionArg }.second } } - - fun givenThrowsFor(wizard: RegistrationWizard, action: RegisterAction, cause: Throwable) { - coEvery { instance.handleRegisterAction(wizard, action) } throws cause - } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizardActionDelegate.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizardActionDelegate.kt new file mode 100644 index 0000000000..3e95be3dae --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizardActionDelegate.kt @@ -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.test.fakes + +import im.vector.app.features.onboarding.RegisterAction +import im.vector.app.features.onboarding.RegistrationResult +import im.vector.app.features.onboarding.RegistrationWizardActionDelegate +import io.mockk.coEvery +import io.mockk.mockk + +class FakeRegistrationWizardActionDelegate { + + val instance = mockk() + + fun givenResultsFor(result: List>) { + coEvery { instance.executeAction(any()) } answers { call -> + val actionArg = call.invocation.args[0] as RegisterAction + result.first { it.first == actionArg }.second + } + } + + fun givenThrowsFor(action: RegisterAction, cause: Throwable) { + coEvery { instance.executeAction(action) } throws cause + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index aeabcce7cd..e227a1a686 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -26,4 +26,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() { fun givenPersonalisationEnabled() { every { isOnboardingPersonalizeEnabled() } returns true } + + fun givenCombinedRegisterEnabled() { + every { isOnboardingCombinedRegisterEnabled() } returns true + } } diff --git a/vector/src/test/java/im/vector/app/test/fixtures/SelectedHomeserverStateFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/SelectedHomeserverStateFixture.kt new file mode 100644 index 0000000000..9d7ff7d291 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/SelectedHomeserverStateFixture.kt @@ -0,0 +1,26 @@ +/* + * 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.fixtures + +import im.vector.app.features.onboarding.SelectedHomeserverState + +object SelectedHomeserverStateFixture { + + fun aSelectedHomeserverState( + userFacingUrl: String = "https://myhomeserver.com", + ) = SelectedHomeserverState(userFacingUrl = userFacingUrl) +} From ef1356f4dd9f5f9819ff5f6bd9c95d04c2cb92a0 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 18 May 2022 13:59:36 +0100 Subject: [PATCH 015/314] replacing comment extracted function (also convered by a test case) --- .../app/features/onboarding/RegistrationActionHandler.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index 9520413cd8..b9569dc15f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -54,14 +54,16 @@ class RegistrationActionHandler @Inject constructor( } private suspend fun processFlowResult(result: RegistrationResult.NextStep, state: SelectedHomeserverState): Result { - // If dummy stage is mandatory, and password is already sent, do the dummy stage now - return if (authenticationService.isRegistrationStarted() && result.flowResult.missingStages.hasMandatoryDummy()) { + return if (shouldFastTrackDummyAction(result)) { processAction(state, RegisterAction.RegisterDummy) } else { handleNextStep(state, result.flowResult) } } + private fun shouldFastTrackDummyAction(result: RegistrationResult.NextStep) = authenticationService.isRegistrationStarted() && + result.flowResult.missingStages.hasMandatoryDummy() + private suspend fun handleNextStep(state: SelectedHomeserverState, flowResult: FlowResult): Result { return when { flowResult.registrationShouldFallback() -> Result.UnsupportedStage From 88167a0287e663e8f3f59c77dd92720973022f68 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 18 May 2022 15:36:29 +0100 Subject: [PATCH 016/314] fixing import ordering --- .../app/features/onboarding/RegistrationActionHandlerTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt index ffb1911b20..601f77b36e 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt @@ -17,18 +17,17 @@ package im.vector.app.features.onboarding import im.vector.app.R -import im.vector.app.test.fixtures.SelectedHomeserverStateFixture.aSelectedHomeserverState import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeRegistrationWizardActionDelegate import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVectorFeatures import im.vector.app.test.fakes.FakeVectorOverrides +import im.vector.app.test.fixtures.SelectedHomeserverStateFixture.aSelectedHomeserverState import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.auth.registration.FlowResult -import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.auth.registration.Stage private val A_SESSION = FakeSession() From befcfe8c5b3d2034c9a1811a6f1175049069707a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Jun 2022 10:29:56 +0100 Subject: [PATCH 017/314] renaming success type to something more concrete --- .../onboarding/OnboardingViewModel.kt | 59 +++++++++---------- .../onboarding/RegistrationActionHandler.kt | 4 +- .../onboarding/OnboardingViewModelTest.kt | 4 +- .../RegistrationActionHandlerTest.kt | 6 +- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 19f6d226ca..fffb1261be 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -273,40 +273,37 @@ class OnboardingViewModel @AssistedInject constructor( } private suspend fun internalRegisterAction(action: RegisterAction, overrideNextStage: (() -> Unit)? = null) { - runCatching { registrationActionHandler.processAction(awaitState().selectedHomeserver, action) } - .fold( - onSuccess = { - when (it) { - RegistrationActionHandler.Result.Ignored -> { - // do nothing - } - is RegistrationActionHandler.Result.NextStage -> { - overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayRegistrationStage(it.stage)) - } - is RegistrationActionHandler.Result.Success -> onSessionCreated( - it.session, - authenticationDescription = awaitState().selectedAuthenticationState.description - ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) - ) - RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) - RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) - is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) - is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) - RegistrationActionHandler.Result.MissingNextStage -> { - _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found"))) - } - } - }, - onFailure = { - if (it !is CancellationException) { - _viewEvents.post(OnboardingViewEvents.Failure(it)) - } + runCatching { registrationActionHandler.processAction(awaitState().selectedHomeserver, action) }.fold( + onSuccess = { + when (it) { + RegistrationActionHandler.Result.Ignored -> { + // do nothing } - ) + is RegistrationActionHandler.Result.NextStage -> { + overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayRegistrationStage(it.stage)) + } + is RegistrationActionHandler.Result.RegistrationComplete -> onSessionCreated( + it.session, + authenticationDescription = awaitState().selectedAuthenticationState.description + ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) + ) + RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) + RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) + is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) + RegistrationActionHandler.Result.MissingNextStage -> { + _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found"))) + } + } + }, + onFailure = { + if (it !is CancellationException) { + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + } + ) } - private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl - private fun handleRegisterWith(action: AuthenticateAction.Register) { setState { val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index b9569dc15f..07a6488676 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -45,7 +45,7 @@ class RegistrationActionHandler @Inject constructor( return when { action.ignoresResult() -> Result.Ignored else -> when (result) { - is RegistrationResult.Complete -> Result.Success(result.session) + is RegistrationResult.Complete -> Result.RegistrationComplete(result.session) is RegistrationResult.NextStep -> processFlowResult(result, state) is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email) is RegistrationResult.Error -> Result.Error(result.cause) @@ -91,7 +91,7 @@ class RegistrationActionHandler @Inject constructor( private fun SelectedHomeserverState.hasSelectedMatrixOrg() = userFacingUrl == matrixOrgUrl sealed interface Result { - data class Success(val session: Session) : Result + data class RegistrationComplete(val session: Session) : Result data class NextStage(val stage: Stage) : Result data class Error(val cause: Throwable) : Result data class SendEmailSuccess(val email: String) : Result diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index ce32dfeb3d..658e14d411 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -31,8 +31,8 @@ import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeDirectLoginUseCase import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerHistoryService -import im.vector.app.test.fakes.FakeRegistrationActionHandler import im.vector.app.test.fakes.FakeLoginWizard +import im.vector.app.test.fakes.FakeRegistrationActionHandler import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStartAuthenticationFlowUseCase import im.vector.app.test.fakes.FakeStringProvider @@ -316,7 +316,7 @@ class OnboardingViewModelTest { fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest { fakeVectorFeatures.givenPersonalisationEnabled() givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) - givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Success(fakeSession)) + givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.RegistrationComplete(fakeSession)) val test = viewModel.test() viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.StartRegistration)) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt index 601f77b36e..7b6f9c615c 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt @@ -61,12 +61,12 @@ class RegistrationActionHandlerTest { } @Test - fun `given wizard delegate returns success, when handling action, then returns success`() = runTest { + fun `given wizard delegate returns success, when handling action, then returns RegistrationComplete`() = runTest { fakeWizardActionDelegate.givenResultsFor(listOf(RegisterAction.StartRegistration to RegistrationResult.Complete(A_SESSION))) val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) - result shouldBeEqualTo RegistrationActionHandler.Result.Success(A_SESSION) + result shouldBeEqualTo RegistrationActionHandler.Result.RegistrationComplete(A_SESSION) } @Test @@ -154,7 +154,7 @@ class RegistrationActionHandlerTest { val result = registrationActionHandler.processAction(RegisterAction.StartRegistration) - result shouldBeEqualTo RegistrationActionHandler.Result.Success(A_SESSION) + result shouldBeEqualTo RegistrationActionHandler.Result.RegistrationComplete(A_SESSION) } private fun givenFlowResult(stages: List) { From 04b297b261954555245e2be9b5df0adb3933f001 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 25 Feb 2022 16:25:56 +0100 Subject: [PATCH 018/314] Add UnifiedPush support --- changelog.d/3448.feature | 1 + dependencies.gradle | 2 + dependencies_groups.gradle | 1 + vector-config/src/main/res/values/config.xml | 4 + vector/build.gradle | 10 +- vector/src/gplay/AndroidManifest.xml | 12 +- .../troubleshoot/TestPushFromPushGateway.kt | 4 +- .../app/push/fcm/EmbeddedFCMDistributor.kt | 27 +++ .../java/im/vector/app/push/fcm/FcmHelper.kt | 4 +- vector/src/main/AndroidManifest.xml | 11 + .../vector/app/core/pushers/PushersManager.kt | 30 ++- .../app/core/pushers/UnifiedPushHelper.kt | 205 ++++++++++++++++++ .../core/pushers/VectorMessagingReceiver.kt} | 134 ++++++++---- .../features/call/webrtc/WebRtcCallManager.kt | 6 +- .../vector/app/features/home/HomeActivity.kt | 11 +- ...rSettingsNotificationPreferenceFragment.kt | 38 ++-- vector/src/main/res/values/strings.xml | 4 + 17 files changed, 431 insertions(+), 73 deletions(-) create mode 100644 changelog.d/3448.feature create mode 100644 vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt create mode 100644 vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt rename vector/src/{gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt => main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt} (60%) mode change 100755 => 100644 diff --git a/changelog.d/3448.feature b/changelog.d/3448.feature new file mode 100644 index 0000000000..3f83f1bef5 --- /dev/null +++ b/changelog.d/3448.feature @@ -0,0 +1 @@ +Use UnifiedPush and allows user to have push without FCM. diff --git a/dependencies.gradle b/dependencies.gradle index 451ad4449b..0b29996438 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,6 +43,7 @@ ext.libs = [ ], jetbrains : [ + 'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$kotlin", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" @@ -88,6 +89,7 @@ ext.libs = [ ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", + 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 59cefe7e89..842a235b16 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -12,6 +12,7 @@ ext.groups = [ 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', + 'com.github.UnifiedPush', ] ], jitsi : [ diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index 78b92cbfa4..cae094f454 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -17,7 +17,11 @@ --> + https://matrix.org/_matrix/push/v1/notify + + + https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify im.vector.app.android diff --git a/vector/build.gradle b/vector/build.gradle index 751d15122b..238a9f05d0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -282,6 +282,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } fdroid { @@ -293,6 +294,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } } @@ -348,6 +350,7 @@ dependencies { implementation project(":library:multipicker") implementation 'androidx.multidex:multidex:2.0.1' + implementation libs.jetbrains.kotlinReflect implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -364,6 +367,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi + implementation libs.squareup.moshiKt kapt libs.squareup.moshiKotlin // Lifecycle @@ -462,8 +466,10 @@ dependencies { // Analytics implementation 'com.posthog.android:posthog:1.1.2' - // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { + // UnifiedPush + implementation 'com.github.UnifiedPush:android-connector:2.0.0-beta2' + // UnifiedPush gplay flavor only + gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:a0056aa939') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index f541eebd83..5b384d1a0a 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -9,13 +9,17 @@ android:name="firebase_analytics_collection_deactivated" android:value="true" /> - + - + + - + + diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index b4b8a936d0..3fd80ad1c5 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -50,12 +50,12 @@ class TestPushFromPushGateway @Inject constructor( override fun perform(activityResultLauncher: ActivityResultLauncher) { pushReceived = false - val fcmToken = FcmHelper.getFcmToken(context) ?: run { + FcmHelper.getFcmToken(context) ?: run { status = TestStatus.FAILED return } action = activeSessionHolder.getActiveSession().coroutineScope.launch { - val result = runCatching { pushersManager.testPush(fcmToken) } + val result = runCatching { pushersManager.testPush(context) } withContext(Dispatchers.Main) { status = result diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt new file mode 100644 index 0000000000..0d0e066eb4 --- /dev/null +++ b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt @@ -0,0 +1,27 @@ +/* + * 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.push.fcm + +import android.content.Context +import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver + +class EmbeddedFCMDistributor: EmbeddedDistributorReceiver() { + override fun getEndpoint(context: Context, token: String, instance: String): String { + // Here token is the FCM Token, used by the gateway (sygnal) + return token + } +} diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index ac2d063700..ef333bb30b 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -98,9 +98,9 @@ object FcmHelper { * it doesn't, display a dialog that allows users to download the APK from * the Google Play Store or enable it in the device's system settings. */ - private fun checkPlayServices(activity: Activity): Boolean { + fun checkPlayServices(context: Context): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity) + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) return resultCode == ConnectionResult.SUCCESS } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 89c600b052..87fd307647 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -410,6 +410,17 @@ + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index c89dc7a73c..a5e03c7d14 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -16,6 +16,7 @@ package im.vector.app.core.pushers +import android.content.Context import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider @@ -34,13 +35,13 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider ) { - suspend fun testPush(pushKey: String) { + suspend fun testPush(context: Context) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - stringProvider.getString(R.string.pusher_http_url), + UnifiedPushHelper.getPushGateway(context), stringProvider.getString(R.string.pusher_app_id), - pushKey, + UnifiedPushHelper.getEndpointOrToken(context) ?: "", TEST_EVENT_ID ) } @@ -50,19 +51,38 @@ class PushersManager @Inject constructor( return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) } + fun enqueueRegisterPusher( + pushKey: String, + gateway: String + ): UUID { + val currentSession = activeSessionHolder.getActiveSession() + return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey, gateway)) + } + suspend fun registerPusherWithFcmKey(pushKey: String) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) } - private fun createHttpPusher(pushKey: String) = HttpPusher( + suspend fun registerPusher( + pushKey: String, + gateway: String + ) { + val currentSession = activeSessionHolder.getActiveSession() + currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey, gateway)) + } + + private fun createHttpPusher( + pushKey: String, + gateway: String = stringProvider.getString(R.string.pusher_http_url) + ) = HttpPusher( pushKey, stringProvider.getString(R.string.pusher_app_id), profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), localeProvider.current().language, appNameProvider.getAppName(), activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", - stringProvider.getString(R.string.pusher_http_url), + gateway, append = false, withEventIdOnly = true ) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt new file mode 100644 index 0000000000..62b174b0f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -0,0 +1,205 @@ +/* + * 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.pushers + +import android.content.Context +import android.content.pm.PackageManager +import androidx.appcompat.app.AlertDialog +import androidx.core.content.edit +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.push.fcm.FcmHelper +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import java.net.URI +import java.net.URL + +object UnifiedPushHelper { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + private val up = UnifiedPush + + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpointOrToken(context: Context): String? { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(context: Context, + endpoint: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(context: Context): String { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_PUSH_GATEWAY, null)!! + } + + /** + * Store Push Gateway to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param gateway the push gateway to store + */ + fun storePushGateway(context: Context, + gateway: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } + + fun register(context: Context, force: Boolean = false, onDoneRunnable: Runnable? = null) { + if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + up.saveDistributor(context, context.packageName) + up.registerApp(context) + onDoneRunnable?.run() + return + } + if (force) { + // Un-register first + up.unregisterApp(context) + storeUpEndpoint(context, null) + storePushGateway(context, null) + } else if (up.getDistributor(context).isNotEmpty()) { + up.registerApp(context) + onDoneRunnable?.run() + return + } + val distributors = up.getDistributors(context).toMutableList() + + val internalDistributorName = if (!FcmHelper.isPushSupported()) { + // Adding packageName for background sync + distributors.add(context.packageName) + context.getString(R.string.unifiedpush_getdistributors_dialog_background_sync) + } else { + context.getString(R.string.unifiedpush_getdistributors_dialog_fcm_fallback) + } + + if (distributors.size == 1 + && !force){ + up.saveDistributor(context, distributors.first()) + up.registerApp(context) + onDoneRunnable?.run() + } else { + val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(context) + builder.setTitle(context.getString(R.string.unifiedpush_getdistributors_dialog_title)) + + val distributorsArray = distributors.toTypedArray() + val distributorsNameArray = distributorsArray.map { + if (it == context.packageName) { + internalDistributorName + } else { + try { + val ai = context.packageManager.getApplicationInfo(it, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + } + }.toTypedArray() + builder.setItems(distributorsNameArray) { _, which -> + val distributor = distributorsArray[which] + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) + onDoneRunnable?.run() + } + builder.setOnDismissListener { + onDoneRunnable?.run() + } + builder.setOnCancelListener { + onDoneRunnable?.run() + } + val dialog: AlertDialog = builder.create() + dialog.show() + } + } + + fun unregister(context: Context) { + up.unregisterApp(context) + } + + fun customOrDefaultGateway(context: Context, endpoint: String?): String { + // if we use the embedded distributor, + // register app_id type upfcm on sygnal + // the pushkey if FCM key + if (up.getDistributor(context) == context.packageName) { + return context.getString(R.string.pusher_http_url) + } + // else, unifiedpush, and pushkey is an endpoint + val default = context.getString(R.string.default_push_gateway_http_url) + endpoint?.let { + val uri = URI(it) + val custom = "${it.split(uri.rawPath)[0]}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + /** + * TODO: + * if GET custom returns """{"unifiedpush":{"gateway":"matrix"}}""" + * return custom + */ + } + return default + } + + fun distributorExists(context: Context): Boolean { + return up.getDistributor(context).isNotEmpty() + } + + fun isEmbeddedDistributor(context: Context) : Boolean { + return ( up.getDistributor(context) == context.packageName + && FcmHelper.isPushSupported()) + } + + fun isBackgroundSync(context: Context) : Boolean { + return ( up.getDistributor(context) == context.packageName + && !FcmHelper.isPushSupported()) + } + + fun getPrivacyFriendlyUpEndpoint(context: Context): String? { + val endpoint = getEndpointOrToken(context) + if (endpoint.isNullOrEmpty()) return endpoint + if (isEmbeddedDistributor(context)) { + return endpoint + } + return try { + val parsed = URL(endpoint) + "${parsed.protocol}://${parsed.host}" + } catch (e: Exception) { + Timber.e("Error parsing unifiedpush endpoint: $e") + null + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt old mode 100755 new mode 100644 similarity index 60% rename from vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt rename to vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 8d0126d6af..4b30f802e3 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * 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 + * 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, @@ -14,27 +14,31 @@ * limitations under the License. */ -package im.vector.app.gplay.push.fcm +package im.vector.app.core.pushers +import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper +import android.widget.Toast import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector -import im.vector.app.core.pushers.PushersManager +import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -43,18 +47,35 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject +@JsonClass(generateAdapter = true) +data class UnifiedPushMessage( + val notification: Notification = Notification() +) + +@JsonClass(generateAdapter = true) +data class Notification( + @Json(name = "event_id") val eventId: String = "", + @Json(name = "room_id") val roomId: String = "", + var unread: Int = 0, + val counts: Counts = Counts() +) + +@JsonClass(generateAdapter = true) +data class Counts( + val unread: Int = 0 +) + private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) /** - * Class extending FirebaseMessagingService. + * Hilt injection happen at super.onReceive(). */ @AndroidEntryPoint -class VectorFirebaseMessagingService : FirebaseMessagingService() { - +class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notifiableEventResolver: NotifiableEventResolver @Inject lateinit var pusherManager: PushersManager @@ -74,21 +95,38 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * Called when message is received. * * @param message the message + * @param instance connection, for multi-account */ - override fun onMessageReceived(message: RemoteMessage) { + override fun onMessage(context: Context, message: ByteArray, instance: String) { + Timber.tag(loggerTag.value).d("## onMessage() received") + val sMessage = String(message) if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessage() %s", sMessage) } - Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) runBlocking { vectorDataStore.incrementPushCounter() } + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + lateinit var notification: Notification + + if (UnifiedPushHelper.isEmbeddedDistributor(context)) { + notification = moshi.adapter(Notification::class.java) + .fromJson(sMessage) ?: return + } else { + val data = moshi.adapter(UnifiedPushMessage::class.java) + .fromJson(sMessage) ?: return + notification = data.notification + notification.unread = notification.counts.unread + } + // Diagnostic Push - if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { + if (notification.eventId == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) - LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) return } @@ -102,7 +140,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // we are in foreground, let the sync do the things? Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { - onMessageReceivedInternal(message.data) + onMessageReceivedInternal(context, notification) } } } @@ -113,55 +151,69 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * when the InstanceID token is initially generated, so this is where * you retrieve the token. */ - override fun onNewToken(refreshedToken: String) { - Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") - FcmHelper.storeFcmToken(this, refreshedToken) + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken) + val gateway = UnifiedPushHelper.customOrDefaultGateway(context, endpoint) + // If the endpoint has changed + // or the gateway has changed + if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint + || UnifiedPushHelper.getPushGateway(context) != gateway) { + UnifiedPushHelper.storePushGateway(context, gateway) + UnifiedPushHelper.storeUpEndpoint(context, endpoint) + pusherManager.enqueueRegisterPusher(endpoint, gateway) + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") + } } + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + vectorPreferences.setFdroidSyncBackgroundMode(mode) } - /** - * Called when the FCM server deletes pending messages. This may be due to: - * - Too many messages stored on the FCM server. - * This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline. - * - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks) - * sent a message to the app on that device. - * - * It is recommended that the app do a full sync with the app server after receiving this call. - */ - override fun onDeletedMessages() { - Timber.tag(loggerTag.value).v("## onDeletedMessages()") + override fun onRegistrationFailed(context: Context, instance: String) { + Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() + } + + override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + vectorPreferences.setFdroidSyncBackgroundMode(mode) + runBlocking { + try { + pusherManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") + } catch (e: Exception) { + Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") + } + } } /** * Internal receive method * - * @param data Data map containing message data as key/value pairs. - * For Set of keys use data.keySet(). + * @param notification Notification containing message data. */ - private fun onMessageReceivedInternal(data: Map) { + private fun onMessageReceivedInternal(context: Context, notification: Notification) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification") } else { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } + // update the badge counter + BadgeProxy.updateBadgeCount(context, notification.unread) + val session = activeSessionHolder.getSafeActiveSession() if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { - val eventId = data["event_id"] - val roomId = data["room_id"] - - if (isEventAlreadyKnown(eventId, roomId)) { + if (isEventAlreadyKnown(notification.eventId, notification.roomId)) { Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, roomId, eventId) + getEventFastLane(session, notification.roomId, notification.eventId) Timber.tag(loggerTag.value).d("Requesting background sync") session.syncService().requireBackgroundSync() diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index fa991501ea..cf532ea744 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.CallService import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CallEnded @@ -32,7 +33,6 @@ import im.vector.app.features.call.lookup.CallUserMapper import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.vectorCallService import im.vector.app.features.session.coroutineScope -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.extensions.orFalse @@ -272,7 +272,7 @@ class WebRtcCallManager @Inject constructor( audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing @@ -378,7 +378,7 @@ class WebRtcCallManager @Inject constructor( // and thus won't be able to received events. For example if the call is // accepted on an other session this device will continue ringing if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { // only for push version as fdroid version is already doing it? currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 6f0e11f3b8..24831a67e3 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -44,6 +44,7 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs @@ -187,7 +188,15 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) + UnifiedPushHelper.register(this, onDoneRunnable = { + if (UnifiedPushHelper.isEmbeddedDistributor(this)) { + FcmHelper.ensureFcmTokenIsRetrieved( + this, + pushManager, + vectorPreferences.areNotificationEnabledForDevice() + ) + } + }) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 2eb62bbb1e..6a40dd2311 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -38,6 +38,7 @@ import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.utils.combineLatest import im.vector.app.core.utils.isIgnoringBatteryOptimizations @@ -49,7 +50,6 @@ import im.vector.app.features.settings.BackgroundSyncModeChooserDialog import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -58,6 +58,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.RuleKind +import timber.log.Timber import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -97,16 +98,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.registerPusherWithFcmKey(it) - } - } else { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.unregisterPusher(it) - session.pushersService().refreshPushers() - } - } + updateEnabledForDevice(isChecked) } } @@ -222,7 +214,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } findPreference(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { - it.isVisible = !FcmHelper.isPushSupported() + it.isVisible = UnifiedPushHelper.isBackgroundSync(requireContext()) } val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled() @@ -331,7 +323,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private fun refreshPref() { // This pref may have change from troubleshoot pref fragment - if (!FcmHelper.isPushSupported()) { + if (UnifiedPushHelper.isBackgroundSync(requireContext())) { findPreference(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) ?.isChecked = vectorPreferences.autoStartOnBoot() } @@ -364,6 +356,26 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + private suspend fun updateEnabledForDevice(enabled: Boolean) { + if (enabled) { + UnifiedPushHelper.register(requireContext()) + } else { + UnifiedPushHelper.getEndpointOrToken(requireContext())?.let { + try { + pushManager.unregisterPusher(it) + } catch (e: Exception) { + Timber.d("Probably unregistering a non existant pusher") + } + try { + UnifiedPushHelper.unregister(requireContext()) + } catch (e: Exception) { + Timber.d("Probably unregistering to a non-saved distributor") + } + session.pushersService().refreshPushers() + } + } + } + private fun updateEnabledForAccount(preference: Preference?) { val pushRuleService = session.pushRuleService() val switchPref = preference as SwitchPreference diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 97ef900ce3..71ed348094 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3063,4 +3063,8 @@ ${app_name} Screen Sharing Screen sharing is in progress + + Choose how to receive notifications + Google Services + Background synchronization From 848adc415f13e09f67efdc6bd7a57d7e8ba0c53b Mon Sep 17 00:00:00 2001 From: sim Date: Sun, 27 Feb 2022 19:29:37 +0100 Subject: [PATCH 019/314] Add UnifiedPush settings --- .../app/core/pushers/UnifiedPushHelper.kt | 32 ++++++++++-- .../core/pushers/VectorMessagingReceiver.kt | 2 +- .../features/settings/VectorPreferences.kt | 3 ++ ...rSettingsNotificationPreferenceFragment.kt | 49 ++++++++++--------- vector/src/main/res/values/strings.xml | 1 + .../res/xml/vector_settings_notifications.xml | 5 ++ 6 files changed, 66 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 62b174b0f5..a629792204 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -24,7 +24,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.runBlocking import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import java.net.URI @@ -81,7 +84,10 @@ object UnifiedPushHelper { } } - fun register(context: Context, force: Boolean = false, onDoneRunnable: Runnable? = null) { + fun register(context: Context, + force: Boolean = false, + pushersManager: PushersManager? = null, + onDoneRunnable: Runnable? = null) { if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { up.saveDistributor(context, context.packageName) up.registerApp(context) @@ -90,14 +96,21 @@ object UnifiedPushHelper { } if (force) { // Un-register first + runBlocking { + pushersManager?.unregisterPusher(getEndpointOrToken(context) ?: "") + } up.unregisterApp(context) storeUpEndpoint(context, null) storePushGateway(context, null) - } else if (up.getDistributor(context).isNotEmpty()) { + } + if (up.getDistributor(context).isNotEmpty()) { up.registerApp(context) onDoneRunnable?.run() return } + + // By default, use internal solution (fcm/background sync) + up.saveDistributor(context, context.packageName) val distributors = up.getDistributors(context).toMutableList() val internalDistributorName = if (!FcmHelper.isPushSupported()) { @@ -148,7 +161,20 @@ object UnifiedPushHelper { } } - fun unregister(context: Context) { + fun unregister( + context: Context, + pushersManager: PushersManager? = null, + vectorPreferences: VectorPreferences? = null + ) { + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences?.setFdroidSyncBackgroundMode(mode) + runBlocking { + try { + pushersManager?.unregisterPusher(getEndpointOrToken(context) ?: "") + } catch (e: Exception) { + Timber.d("Probably unregistering a non existant pusher") + } + } up.unregisterApp(context) } diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 4b30f802e3..57953c1ca7 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -176,7 +176,7 @@ class VectorMessagingReceiver : MessagingReceiver() { override fun onUnregistered(context: Context, instance: String) { Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) runBlocking { try { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index ce9c068c9c..1b61eb9bcf 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -141,6 +141,9 @@ class VectorPreferences @Inject constructor( const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" + // notification method + const val SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY = "SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY" + // Calls const val SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY = "SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY" const val SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 6a40dd2311..216b645726 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult @@ -58,7 +59,6 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.RuleKind -import timber.log.Timber import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -98,7 +98,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - updateEnabledForDevice(isChecked) + if (isChecked) { + UnifiedPushHelper.register(requireContext()) + } else { + UnifiedPushHelper.unregister( + requireContext(), + pushManager, + vectorPreferences + ) + session.pushersService().refreshPushers() + } } } @@ -140,6 +149,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { + if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + UnifiedPushHelper.register( + requireContext(), + force = true, + pushManager + ) + true + } + session.pushersService().refreshPushers() + } else { + it.isVisible = false + } + } + bindEmailNotifications() refreshBackgroundSyncPrefs() @@ -356,26 +381,6 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } - private suspend fun updateEnabledForDevice(enabled: Boolean) { - if (enabled) { - UnifiedPushHelper.register(requireContext()) - } else { - UnifiedPushHelper.getEndpointOrToken(requireContext())?.let { - try { - pushManager.unregisterPusher(it) - } catch (e: Exception) { - Timber.d("Probably unregistering a non existant pusher") - } - try { - UnifiedPushHelper.unregister(requireContext()) - } catch (e: Exception) { - Timber.d("Probably unregistering to a non-saved distributor") - } - session.pushersService().refreshPushers() - } - } - } - private fun updateEnabledForAccount(preference: Preference?) { val pushRuleService = session.pushRuleService() val switchPref = preference as SwitchPreference diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 71ed348094..412156be2a 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3067,4 +3067,5 @@ Choose how to receive notifications Google Services Background synchronization + Notification method diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 66ac93a4f9..c8434b6920 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -77,6 +77,11 @@ android:summary="@string/settings_system_preferences_summary" android:title="@string/settings_call_notifications_preferences" /> + + Date: Mon, 28 Feb 2022 15:46:53 +0100 Subject: [PATCH 020/314] Add UnifiedPush troubleshoot --- vector/src/fdroid/AndroidManifest.xml | 13 +++ .../receiver/KeepInternalDistributor.kt | 32 +++++++ ...ificationTroubleshootTestManagerFactory.kt | 31 ++++++- ...ificationTroubleshootTestManagerFactory.kt | 35 ++++++-- .../vector/app/core/pushers/PushersManager.kt | 2 +- .../app/core/pushers/UnifiedPushHelper.kt | 71 ++++++++++++---- .../core/pushers/VectorMessagingReceiver.kt | 13 ++- .../vector/app/features/home/HomeActivity.kt | 4 +- ...rSettingsNotificationPreferenceFragment.kt | 20 +++-- .../TestAvailableUnifiedPushDistributors.kt | 47 +++++++++++ .../TestCurrentUnifiedPushDistributor.kt | 36 ++++++++ .../TestEndpointAsTokenRegistration.kt | 83 +++++++++++++++++++ .../troubleshoot/TestPushFromPushGateway.kt | 20 ++--- .../troubleshoot/TestUnifiedPushEndpoint.kt | 42 ++++++++++ .../troubleshoot/TestUnifiedPushGateway.kt | 36 ++++++++ vector/src/main/res/values/strings.xml | 21 ++++- 16 files changed, 448 insertions(+), 58 deletions(-) create mode 100644 vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt rename vector/src/{gplay/java/im/vector/app/gplay => main/java/im/vector/app}/features/settings/troubleshoot/TestPushFromPushGateway.kt (85%) create mode 100644 vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index ea9fa023ab..ca008043c2 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -28,6 +28,19 @@ android:enabled="true" android:exported="false" /> + + + + + + + { + val distributors = up.getDistributors(context).toMutableList() + distributors.remove(context.packageName) + return distributors + } + + fun getCurrentDistributorName(context: Context): String { + if (isEmbeddedDistributor(context)) { + return context.getString(R.string.unifiedpush_distributor_fcm_fallback) + } + if (isBackgroundSync(context)) { + return context.getString(R.string.unifiedpush_distributor_background_sync) + } + val distributor = up.getDistributor(context) + return try { + val ai = context.packageManager.getApplicationInfo(distributor, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + distributor + } as String } fun isEmbeddedDistributor(context: Context) : Boolean { @@ -222,7 +257,7 @@ object UnifiedPushHelper { } return try { val parsed = URL(endpoint) - "${parsed.protocol}://${parsed.host}" + "${parsed.protocol}://${parsed.host}/***" } catch (e: Exception) { Timber.e("Error parsing unifiedpush endpoint: $e") null diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 57953c1ca7..a9d3845bba 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -32,6 +32,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector +import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager @@ -78,11 +79,12 @@ private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notifiableEventResolver: NotifiableEventResolver - @Inject lateinit var pusherManager: PushersManager + @Inject lateinit var pushersManager: PushersManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorDataStore: VectorDataStore @Inject lateinit var wifiDetector: WifiDetector + @Inject lateinit var guardServiceStarter: GuardServiceStarter private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -161,26 +163,31 @@ class VectorMessagingReceiver : MessagingReceiver() { || UnifiedPushHelper.getPushGateway(context) != gateway) { UnifiedPushHelper.storePushGateway(context, gateway) UnifiedPushHelper.storeUpEndpoint(context, endpoint) - pusherManager.enqueueRegisterPusher(endpoint, gateway) + pushersManager.enqueueRegisterPusher(endpoint, gateway) } else { Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } } val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.stop() } override fun onRegistrationFailed(context: Context, instance: String) { Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() } override fun onUnregistered(context: Context, instance: String) { Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() runBlocking { try { - pusherManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") + pushersManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") } catch (e: Exception) { Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 24831a67e3..d1d1e2e4ce 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -188,7 +188,7 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - UnifiedPushHelper.register(this, onDoneRunnable = { + UnifiedPushHelper.register(this) { if (UnifiedPushHelper.isEmbeddedDistributor(this)) { FcmHelper.ensureFcmTokenIsRetrieved( this, @@ -196,7 +196,7 @@ class HomeActivity : vectorPreferences.areNotificationEnabledForDevice() ) } - }) + } sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 216b645726..282e62d43d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -63,7 +63,7 @@ import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml class VectorSettingsNotificationPreferenceFragment @Inject constructor( - private val pushManager: PushersManager, + private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, private val guardServiceStarter: GuardServiceStarter @@ -103,7 +103,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } else { UnifiedPushHelper.unregister( requireContext(), - pushManager, + pushersManager, vectorPreferences ) session.pushersService().refreshPushers() @@ -152,14 +152,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - UnifiedPushHelper.register( + UnifiedPushHelper.reRegister( requireContext(), - force = true, - pushManager - ) + pushersManager, + vectorPreferences + ) { + session.pushersService().refreshPushers() + refreshBackgroundSyncPrefs() + } true } - session.pushersService().refreshPushers() } else { it.isVisible = false } @@ -199,9 +201,9 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( pref.isChecked = isEnabled pref.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - pushManager.registerEmailForPush(emailPid.email) + pushersManager.registerEmailForPush(emailPid.email) } else { - pushManager.unregisterEmailPusher(emailPid.email) + pushersManager.unregisterEmailPusher(emailPid.email) } } category.addPreference(pref) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt new file mode 100644 index 0000000000..9e25b27d0f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -0,0 +1,47 @@ +/* + * 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.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import im.vector.app.push.fcm.FcmHelper +import javax.inject.Inject + +class TestAvailableUnifiedPushDistributors @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val distributors = UnifiedPushHelper.getExternalDistributors(context) + if (distributors.isEmpty()) { + description = if (FcmHelper.isPushSupported()) { + stringProvider.getString(R.string.settings_troubleshoot_test_distributors_gplay) + } else { + stringProvider.getString(R.string.settings_troubleshoot_test_distributors_fdroid) + } + status = TestStatus.SUCCESS + } else { + description = stringProvider.getString(R.string.settings_troubleshoot_test_distributors_many, + distributors.size + 1) + status = TestStatus.SUCCESS + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt new file mode 100644 index 0000000000..5eda48ffdc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt @@ -0,0 +1,36 @@ +/* + * 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.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestCurrentUnifiedPushDistributor @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_distributor_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_distributor, + UnifiedPushHelper.getCurrentDistributorName(context)) + status = TestStatus.SUCCESS + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt new file mode 100644 index 0000000000..558b0e4fd9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -0,0 +1,83 @@ +/* + * 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.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Observer +import androidx.work.WorkInfo +import androidx.work.WorkManager +import im.vector.app.R +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.core.resources.StringProvider +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.pushers.PusherState +import javax.inject.Inject + +class TestEndpointAsTokenRegistration @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder) : + TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + // Check if we have a registered pusher for this token + val endpoint = UnifiedPushHelper.getEndpointOrToken(context) ?: run { + status = TestStatus.FAILED + return + } + val session = activeSessionHolder.getSafeActiveSession() ?: run { + status = TestStatus.FAILED + return + } + val pushers = session.pushersService().getPushers().filter { + it.pushKey == endpoint && it.state == PusherState.REGISTERED + } + if (pushers.isEmpty()) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_failed, + stringProvider.getString(R.string.sas_error_unknown)) + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { + override fun doFix() { + UnifiedPushHelper.reRegister( + context, + pushersManager, + vectorPreferences + ) + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint) + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> + if (workInfo != null) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + manager?.retry(activityResultLauncher) + } else if (workInfo.state == WorkInfo.State.FAILED) { + manager?.retry(activityResultLauncher) + } + } + }) + } + } + + status = TestStatus.FAILED + } else { + description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success) + status = TestStatus.SUCCESS + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt similarity index 85% rename from vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt rename to vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt index 3fd80ad1c5..da26200df2 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.app.gplay.features.settings.troubleshoot +package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher @@ -24,8 +24,6 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.troubleshoot.TroubleshootTest -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -36,13 +34,11 @@ import javax.inject.Inject /** * Test Push by asking the Push Gateway to send a Push back */ -class TestPushFromPushGateway @Inject constructor( - private val context: FragmentActivity, - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter, - private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder -) : +class TestPushFromPushGateway @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter, + private val pushersManager: PushersManager, + private val activeSessionHolder: ActiveSessionHolder) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null @@ -50,10 +46,6 @@ class TestPushFromPushGateway @Inject constructor( override fun perform(activityResultLauncher: ActivityResultLauncher) { pushReceived = false - FcmHelper.getFcmToken(context) ?: run { - status = TestStatus.FAILED - return - } action = activeSessionHolder.getActiveSession().coroutineScope.launch { val result = runCatching { pushersManager.testPush(context) } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt new file mode 100644 index 0000000000..ab4e3dd7c2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt @@ -0,0 +1,42 @@ +/* + * 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.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestUnifiedPushEndpoint @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_endpoint_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val endpoint = UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context) + endpoint?.let { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_success, + UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context)) + status = TestStatus.SUCCESS + } ?: run { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_failed) + status = TestStatus.FAILED + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt new file mode 100644 index 0000000000..75e8962c84 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt @@ -0,0 +1,36 @@ +/* + * 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.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestUnifiedPushGateway @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_gateway_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_gateway, + UnifiedPushHelper.getPushGateway(context)) + status = TestStatus.SUCCESS + } +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 412156be2a..89cbefd859 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -855,6 +855,10 @@ FCM token successfully registered to homeserver. Failed to register FCM token to homeserver:\n%1$s + Endpoint Registration + Endpoint successfully registered to homeserver. + Failed to register endpoint token to homeserver:\n%1$s + Test Push The application is waiting for the PUSH The application is receiving PUSH @@ -1665,6 +1669,8 @@ Register token + Reset notification method + Make a suggestion Please write your suggestion below. Describe your suggestion here @@ -3065,7 +3071,18 @@ Screen sharing is in progress Choose how to receive notifications - Google Services - Background synchronization + Google Services + Background synchronization Notification method + Available methods + No other method than Google Play Service found. + No other method than background synchronization found. + Found %d methods. + Method + Currently using %s. + Endpoint + Current endpoint: %s + Cannot find the endpoint. + Gateway + Current gateway: %s From 1069f77bd5735e7b9c768bbef67336f0053e756a Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 28 Feb 2022 20:48:02 +0100 Subject: [PATCH 021/314] Lint --- .../app/push/fcm/EmbeddedFCMDistributor.kt | 2 +- ...NotificationTroubleshootTestManagerFactory.kt | 3 +-- .../vector/app/core/pushers/UnifiedPushHelper.kt | 16 ++++++++-------- .../app/core/pushers/VectorMessagingReceiver.kt | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt index 0d0e066eb4..14600ccbb3 100644 --- a/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt @@ -19,7 +19,7 @@ package im.vector.app.push.fcm import android.content.Context import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver -class EmbeddedFCMDistributor: EmbeddedDistributorReceiver() { +class EmbeddedFCMDistributor : EmbeddedDistributorReceiver() { override fun getEndpoint(context: Context, token: String, instance: String): String { // Here token is the FCM Token, used by the gateway (sygnal) return token diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 5a31affcf7..7c9e8b8c26 100644 --- a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -25,11 +25,11 @@ import im.vector.app.features.settings.troubleshoot.TestCurrentUnifiedPushDistri import im.vector.app.features.settings.troubleshoot.TestDeviceSettings import im.vector.app.features.settings.troubleshoot.TestEndpointAsTokenRegistration import im.vector.app.features.settings.troubleshoot.TestNotification +import im.vector.app.features.settings.troubleshoot.TestPushFromPushGateway import im.vector.app.features.settings.troubleshoot.TestPushRulesSettings import im.vector.app.features.settings.troubleshoot.TestSystemSettings import im.vector.app.features.settings.troubleshoot.TestUnifiedPushEndpoint import im.vector.app.features.settings.troubleshoot.TestUnifiedPushGateway -import im.vector.app.features.settings.troubleshoot.TestPushFromPushGateway import im.vector.app.gplay.features.settings.troubleshoot.TestFirebaseToken import im.vector.app.gplay.features.settings.troubleshoot.TestPlayServices import im.vector.app.gplay.features.settings.troubleshoot.TestTokenRegistration @@ -66,7 +66,6 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testPlayServices) mgr.addTest(testFirebaseToken) mgr.addTest(testTokenRegistration) - } else { mgr.addTest(testUnifiedPushGateway) mgr.addTest(testUnifiedPushEndpoint) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 9e0836e4f0..9a47a6232f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -132,8 +132,8 @@ object UnifiedPushHelper { context.getString(R.string.unifiedpush_distributor_background_sync) } - if (distributors.size == 1 - && !force){ + if (distributors.size == 1 && + !force) { up.saveDistributor(context, distributors.first()) up.registerApp(context) onDoneRunnable?.run() @@ -239,14 +239,14 @@ object UnifiedPushHelper { } as String } - fun isEmbeddedDistributor(context: Context) : Boolean { - return ( up.getDistributor(context) == context.packageName - && FcmHelper.isPushSupported()) + fun isEmbeddedDistributor(context: Context): Boolean { + return (up.getDistributor(context) == context.packageName && + FcmHelper.isPushSupported()) } - fun isBackgroundSync(context: Context) : Boolean { - return ( up.getDistributor(context) == context.packageName - && !FcmHelper.isPushSupported()) + fun isBackgroundSync(context: Context): Boolean { + return (up.getDistributor(context) == context.packageName && + !FcmHelper.isPushSupported()) } fun getPrivacyFriendlyUpEndpoint(context: Context): String? { diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index a9d3845bba..ec29b7c0d9 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -159,8 +159,8 @@ class VectorMessagingReceiver : MessagingReceiver() { val gateway = UnifiedPushHelper.customOrDefaultGateway(context, endpoint) // If the endpoint has changed // or the gateway has changed - if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint - || UnifiedPushHelper.getPushGateway(context) != gateway) { + if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint || + UnifiedPushHelper.getPushGateway(context) != gateway) { UnifiedPushHelper.storePushGateway(context, gateway) UnifiedPushHelper.storeUpEndpoint(context, endpoint) pushersManager.enqueueRegisterPusher(endpoint, gateway) From f774f466274c4ed743e5cacd0de2a0b2491baecf Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 28 Feb 2022 22:53:18 +0100 Subject: [PATCH 022/314] Check custom unifiedpush gateway --- .../app/core/pushers/UnifiedPushHelper.kt | 72 ++++++++++++++----- .../core/pushers/VectorMessagingReceiver.kt | 11 +-- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 9a47a6232f..c28cd8325e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -21,16 +21,23 @@ import android.content.pm.PackageManager import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import okhttp3.Request import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber -import java.net.URI import java.net.URL object UnifiedPushHelper { @@ -77,7 +84,7 @@ object UnifiedPushHelper { * @param context android context * @param gateway the push gateway to store */ - fun storePushGateway(context: Context, + private fun storePushGateway(context: Context, gateway: String?) { DefaultSharedPreferences.getInstance(context).edit { putString(PREFS_PUSH_GATEWAY, gateway) @@ -191,30 +198,63 @@ object UnifiedPushHelper { up.unregisterApp(context) } - fun customOrDefaultGateway(context: Context, endpoint: String?): String { + @JsonClass(generateAdapter = true) + internal data class DiscoveryResponse( + val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() + ) + + @JsonClass(generateAdapter = true) + internal data class DiscoveryUnifiedPush( + val gateway: String = "" + ) + + fun storeCustomOrDefaultGateway( + context: Context, + endpoint: String, + onDoneRunnable: Runnable? = null) { // if we use the embedded distributor, // register app_id type upfcm on sygnal // the pushkey if FCM key if (up.getDistributor(context) == context.packageName) { context.getString(R.string.pusher_http_url).let { storePushGateway(context, it) - return it + onDoneRunnable?.run() + return } } // else, unifiedpush, and pushkey is an endpoint - val default = context.getString(R.string.default_push_gateway_http_url) - endpoint?.let { - val uri = URI(it) - val custom = "${it.split(uri.rawPath)[0]}/_matrix/push/v1/notify" - Timber.i("Testing $custom") - /** - * TODO: - * if GET custom returns """{"unifiedpush":{"gateway":"matrix"}}""" - * return custom - */ + val gateway = context.getString(R.string.default_push_gateway_http_url) + val parsed = URL(endpoint) + val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + val thread = CoroutineScope(SupervisorJob()).launch { + try { + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val client = OkHttpClient() + val request = Request.Builder() + .url(custom) + .build() + val sResponse = client.newCall(request).execute() + .body?.string() ?: "" + moshi.adapter(DiscoveryResponse::class.java) + .fromJson(sResponse)?.let { response -> + if (response.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + storePushGateway(context, custom) + onDoneRunnable?.run() + return@launch + } + } + } catch (e: Exception) { + Timber.d("Cannot try custom gateway: $e") + } + storePushGateway(context, gateway) + onDoneRunnable?.run() + return@launch } - storePushGateway(context, default) - return default + thread.start() } fun getExternalDistributors(context: Context): List { diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index ec29b7c0d9..ef18b500c7 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -156,14 +156,15 @@ class VectorMessagingReceiver : MessagingReceiver() { override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - val gateway = UnifiedPushHelper.customOrDefaultGateway(context, endpoint) // If the endpoint has changed // or the gateway has changed - if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint || - UnifiedPushHelper.getPushGateway(context) != gateway) { - UnifiedPushHelper.storePushGateway(context, gateway) + if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint) { UnifiedPushHelper.storeUpEndpoint(context, endpoint) - pushersManager.enqueueRegisterPusher(endpoint, gateway) + UnifiedPushHelper.storeCustomOrDefaultGateway(context, endpoint) { + UnifiedPushHelper.getPushGateway(context)?.let { + pushersManager.enqueueRegisterPusher(endpoint, it) + } + } } else { Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } From 7eca4056934c5b10076acc3037e3fab5916f1b56 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 1 Mar 2022 00:23:39 +0100 Subject: [PATCH 023/314] Update UnifiedPush libs Signed-off-by: sim --- vector/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 238a9f05d0..7593d631fb 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -467,9 +467,9 @@ dependencies { implementation 'com.posthog.android:posthog:1.1.2' // UnifiedPush - implementation 'com.github.UnifiedPush:android-connector:2.0.0-beta2' + implementation 'com.github.UnifiedPush:android-connector:2.0.0' // UnifiedPush gplay flavor only - gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:a0056aa939') { + gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From ee7fccf072cabb64e7f33281dab0e1e0fe2dd294 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:10:19 +0200 Subject: [PATCH 024/314] Fix compilation issues after rebase --- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 4 ++-- .../app/core/pushers/VectorMessagingReceiver.kt | 12 +++++------- .../settings/troubleshoot/TestPushFromPushGateway.kt | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index c28cd8325e..16ed9fb6ab 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -55,7 +55,7 @@ object UnifiedPushHelper { } /** - * Store UnifiedPush Endpoint to the SharedPrefs + * Store UnifiedPush Endpoint to the SharedPrefs. * TODO Store in realm * * @param context android context @@ -78,7 +78,7 @@ object UnifiedPushHelper { } /** - * Store Push Gateway to the SharedPrefs + * Store Push Gateway to the SharedPrefs. * TODO Store in realm * * @param context android context diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index ef18b500c7..729c0bc435 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -33,7 +33,6 @@ import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector import im.vector.app.core.services.GuardServiceStarter -import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils @@ -48,6 +47,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject @@ -96,6 +96,7 @@ class VectorMessagingReceiver : MessagingReceiver() { /** * Called when message is received. * + * @param context the Android context * @param message the message * @param instance connection, for multi-account */ @@ -142,7 +143,7 @@ class VectorMessagingReceiver : MessagingReceiver() { // we are in foreground, let the sync do the things? Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { - onMessageReceivedInternal(context, notification) + onMessageReceivedInternal(notification) } } } @@ -196,11 +197,11 @@ class VectorMessagingReceiver : MessagingReceiver() { } /** - * Internal receive method + * Internal receive method. * * @param notification Notification containing message data. */ - private fun onMessageReceivedInternal(context: Context, notification: Notification) { + private fun onMessageReceivedInternal(notification: Notification) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification") @@ -208,9 +209,6 @@ class VectorMessagingReceiver : MessagingReceiver() { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } - // update the badge counter - BadgeProxy.updateBadgeCount(context, notification.unread) - val session = activeSessionHolder.getSafeActiveSession() if (session == null) { diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt index da26200df2..e8696eafc7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure import javax.inject.Inject /** - * Test Push by asking the Push Gateway to send a Push back + * Test Push by asking the Push Gateway to send a Push back. */ class TestPushFromPushGateway @Inject constructor(private val context: FragmentActivity, private val stringProvider: StringProvider, From d88d27985ecab2fd897dac090d02c2a85c59cfaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:30:21 +0200 Subject: [PATCH 025/314] Sort alphabetically. --- dependencies_groups.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 842a235b16..e6817f2b23 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -9,10 +9,10 @@ ext.groups = [ 'com.github.jetradarmobile', 'com.github.MatrixFrog', 'com.github.tapadoo', + 'com.github.UnifiedPush', 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', - 'com.github.UnifiedPush', ] ], jitsi : [ From 42811751fbdf73e58a3fb29d7495dbf83429fcce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:33:20 +0200 Subject: [PATCH 026/314] Move `ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB` to `defaultConfig` and document it. --- vector/build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 7593d631fb..c0112a2934 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -165,6 +165,10 @@ android { buildConfigField "Boolean", "enableLocationSharing", "true" buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\"" + // Set to false to prevent usage of UnifiedPush. For Gplay variant it means that only FCM will be used, + // And for F-Droid variant, it means that only background polling will be available to the user. + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk @@ -282,7 +286,6 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" - buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } fdroid { @@ -294,7 +297,6 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" - buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } } From 09a918bac446af4cd53ec14aa4a11ac210f7cfec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:35:49 +0200 Subject: [PATCH 027/314] Cleanup --- .../im/vector/app/fdroid/receiver/KeepInternalDistributor.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt index 64e6a73973..3feee8c63b 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt @@ -26,7 +26,5 @@ import android.content.Intent * This class is used to declare this action. */ class KeepInternalDistributor : BroadcastReceiver() { - override fun onReceive(p0: Context?, p1: Intent?) { - return - } + override fun onReceive(context: Context, intent: Intent) {} } From 96acb61fa10d828dfe316543fafa6c75e86ae3a2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:36:19 +0200 Subject: [PATCH 028/314] Add `unifiedpush` to the project dict. --- .idea/dictionaries/bmarty.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 85290e72df..c29bca95f2 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -40,6 +40,7 @@ sygnal threepid uisi + unifiedpush unpublish unwedging vctr From 674e3a72c4b611aa3000eab0101b7cb41dc84215 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:53:22 +0200 Subject: [PATCH 029/314] Make `UnifiedPushHelper` a regular class and inject the context in the constructor to clean up the API --- ...ificationTroubleshootTestManagerFactory.kt | 3 +- ...ificationTroubleshootTestManagerFactory.kt | 3 +- .../vector/app/core/pushers/PushersManager.kt | 10 +- .../app/core/pushers/UnifiedPushHelper.kt | 126 +++++++++--------- .../core/pushers/VectorMessagingReceiver.kt | 13 +- .../features/call/webrtc/WebRtcCallManager.kt | 7 +- .../vector/app/features/home/HomeActivity.kt | 5 +- ...rSettingsNotificationPreferenceFragment.kt | 13 +- .../TestAvailableUnifiedPushDistributors.kt | 16 ++- .../TestCurrentUnifiedPushDistributor.kt | 14 +- .../TestEndpointAsTokenRegistration.kt | 25 ++-- .../troubleshoot/TestPushFromPushGateway.kt | 15 +-- .../troubleshoot/TestUnifiedPushEndpoint.kt | 16 ++- .../troubleshoot/TestUnifiedPushGateway.kt | 14 +- 14 files changed, 150 insertions(+), 130 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 7c9e33043e..b049edaf9a 100644 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -36,6 +36,7 @@ import im.vector.app.features.settings.troubleshoot.TestUnifiedPushGateway import javax.inject.Inject class NotificationTroubleshootTestManagerFactory @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, private val testSystemSettings: TestSystemSettings, private val testAccountSettings: TestAccountSettings, private val testDeviceSettings: TestDeviceSettings, @@ -62,7 +63,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testAvailableUnifiedPushDistributors) mgr.addTest(testCurrentUnifiedPushDistributor) } - if (UnifiedPushHelper.isBackgroundSync(fragment.requireContext())) { + if (unifiedPushHelper.isBackgroundSync()) { mgr.addTest(testAutoStartBoot) mgr.addTest(testBackgroundRestrictions) mgr.addTest(testBatteryOptimization) diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 7c9e8b8c26..767078b0d6 100644 --- a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -36,6 +36,7 @@ import im.vector.app.gplay.features.settings.troubleshoot.TestTokenRegistration import javax.inject.Inject class NotificationTroubleshootTestManagerFactory @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, private val testSystemSettings: TestSystemSettings, private val testAccountSettings: TestAccountSettings, private val testDeviceSettings: TestDeviceSettings, @@ -62,7 +63,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testAvailableUnifiedPushDistributors) mgr.addTest(testCurrentUnifiedPushDistributor) } - if (UnifiedPushHelper.isEmbeddedDistributor(fragment.requireContext())) { + if (unifiedPushHelper.isEmbeddedDistributor()) { mgr.addTest(testPlayServices) mgr.addTest(testFirebaseToken) mgr.addTest(testTokenRegistration) diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 62707df4ad..cb3d7dfe8e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -16,7 +16,6 @@ package im.vector.app.core.pushers -import android.content.Context import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider @@ -30,18 +29,19 @@ import kotlin.math.abs private const val DEFAULT_PUSHER_FILE_TAG = "mobile" class PushersManager @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, private val activeSessionHolder: ActiveSessionHolder, private val localeProvider: LocaleProvider, private val stringProvider: StringProvider, - private val appNameProvider: AppNameProvider + private val appNameProvider: AppNameProvider, ) { - suspend fun testPush(context: Context) { + suspend fun testPush() { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - UnifiedPushHelper.getPushGateway(context)!!, + unifiedPushHelper.getPushGateway()!!, stringProvider.getString(R.string.pusher_app_id), - UnifiedPushHelper.getEndpointOrToken(context) ?: "", + unifiedPushHelper.getEndpointOrToken() ?: "", TEST_EVENT_ID ) } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 16ed9fb6ab..20cad9e6ca 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -27,6 +27,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper @@ -39,10 +40,17 @@ import okhttp3.Request import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import java.net.URL +import javax.inject.Inject + +class UnifiedPushHelper @Inject constructor( + private val context: Context, + private val stringProvider: StringProvider, +) { + companion object { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + } -object UnifiedPushHelper { - private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" - private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" private val up = UnifiedPush /** @@ -50,7 +58,7 @@ object UnifiedPushHelper { * * @return the UnifiedPush Endpoint or null if not received */ - fun getEndpointOrToken(context: Context): String? { + fun getEndpointOrToken(): String? { return DefaultSharedPreferences.getInstance(context).getString(PREFS_ENDPOINT_OR_TOKEN, null) } @@ -58,11 +66,9 @@ object UnifiedPushHelper { * Store UnifiedPush Endpoint to the SharedPrefs. * TODO Store in realm * - * @param context android context * @param endpoint the endpoint to store */ - fun storeUpEndpoint(context: Context, - endpoint: String?) { + fun storeUpEndpoint(endpoint: String?) { DefaultSharedPreferences.getInstance(context).edit { putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) } @@ -73,7 +79,7 @@ object UnifiedPushHelper { * * @return the Push Gateway or null if not defined */ - fun getPushGateway(context: Context): String? { + fun getPushGateway(): String? { return DefaultSharedPreferences.getInstance(context).getString(PREFS_PUSH_GATEWAY, null) } @@ -81,26 +87,26 @@ object UnifiedPushHelper { * Store Push Gateway to the SharedPrefs. * TODO Store in realm * - * @param context android context * @param gateway the push gateway to store */ - private fun storePushGateway(context: Context, - gateway: String?) { + private fun storePushGateway(gateway: String?) { DefaultSharedPreferences.getInstance(context).edit { putString(PREFS_PUSH_GATEWAY, gateway) } } - fun register(context: Context, onDoneRunnable: Runnable? = null) { - gRegister(context, - onDoneRunnable = onDoneRunnable) + fun register(onDoneRunnable: Runnable? = null) { + gRegister( + onDoneRunnable = onDoneRunnable + ) } - fun reRegister(context: Context, - pushersManager: PushersManager, - vectorPreferences: VectorPreferences, - onDoneRunnable: Runnable? = null) { - gRegister(context, + fun reRegister( + pushersManager: PushersManager, + vectorPreferences: VectorPreferences, + onDoneRunnable: Runnable? = null + ) { + gRegister( force = true, pushersManager = pushersManager, vectorPreferences = vectorPreferences, @@ -108,11 +114,12 @@ object UnifiedPushHelper { ) } - private fun gRegister(context: Context, - force: Boolean = false, - pushersManager: PushersManager? = null, - vectorPreferences: VectorPreferences? = null, - onDoneRunnable: Runnable? = null) { + private fun gRegister( + force: Boolean = false, + pushersManager: PushersManager? = null, + vectorPreferences: VectorPreferences? = null, + onDoneRunnable: Runnable? = null + ) { if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { up.saveDistributor(context, context.packageName) up.registerApp(context) @@ -121,7 +128,7 @@ object UnifiedPushHelper { } if (force) { // Un-register first - unregister(context, pushersManager, vectorPreferences) + unregister(pushersManager, vectorPreferences) } if (up.getDistributor(context).isNotEmpty()) { up.registerApp(context) @@ -134,9 +141,9 @@ object UnifiedPushHelper { val distributors = up.getDistributors(context).toMutableList() val internalDistributorName = if (FcmHelper.isPushSupported()) { - context.getString(R.string.unifiedpush_distributor_fcm_fallback) + stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) } else { - context.getString(R.string.unifiedpush_distributor_background_sync) + stringProvider.getString(R.string.unifiedpush_distributor_background_sync) } if (distributors.size == 1 && @@ -146,7 +153,7 @@ object UnifiedPushHelper { onDoneRunnable?.run() } else { val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(context) - builder.setTitle(context.getString(R.string.unifiedpush_getdistributors_dialog_title)) + builder.setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) val distributorsArray = distributors.toTypedArray() val distributorsNameArray = distributorsArray.map { @@ -180,7 +187,6 @@ object UnifiedPushHelper { } fun unregister( - context: Context, pushersManager: PushersManager? = null, vectorPreferences: VectorPreferences? = null ) { @@ -188,13 +194,13 @@ object UnifiedPushHelper { vectorPreferences?.setFdroidSyncBackgroundMode(mode) runBlocking { try { - pushersManager?.unregisterPusher(getEndpointOrToken(context) ?: "") + pushersManager?.unregisterPusher(getEndpointOrToken() ?: "") } catch (e: Exception) { Timber.d("Probably unregistering a non existant pusher") } } - storeUpEndpoint(context, null) - storePushGateway(context, null) + storeUpEndpoint(null) + storePushGateway(null) up.unregisterApp(context) } @@ -209,21 +215,21 @@ object UnifiedPushHelper { ) fun storeCustomOrDefaultGateway( - context: Context, endpoint: String, - onDoneRunnable: Runnable? = null) { + onDoneRunnable: Runnable? = null + ) { // if we use the embedded distributor, // register app_id type upfcm on sygnal // the pushkey if FCM key if (up.getDistributor(context) == context.packageName) { - context.getString(R.string.pusher_http_url).let { - storePushGateway(context, it) + stringProvider.getString(R.string.pusher_http_url).let { + storePushGateway(it) onDoneRunnable?.run() return } } // else, unifiedpush, and pushkey is an endpoint - val gateway = context.getString(R.string.default_push_gateway_http_url) + val gateway = stringProvider.getString(R.string.default_push_gateway_http_url) val parsed = URL(endpoint) val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" Timber.i("Testing $custom") @@ -236,39 +242,39 @@ object UnifiedPushHelper { val request = Request.Builder() .url(custom) .build() - val sResponse = client.newCall(request).execute() - .body?.string() ?: "" - moshi.adapter(DiscoveryResponse::class.java) - .fromJson(sResponse)?.let { response -> - if (response.unifiedpush.gateway == "matrix") { - Timber.d("Using custom gateway") - storePushGateway(context, custom) - onDoneRunnable?.run() - return@launch - } + val sResponse = client.newCall(request).execute() + .body?.string() ?: "" + moshi.adapter(DiscoveryResponse::class.java) + .fromJson(sResponse)?.let { response -> + if (response.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + storePushGateway(custom) + onDoneRunnable?.run() + return@launch } + } } catch (e: Exception) { Timber.d("Cannot try custom gateway: $e") } - storePushGateway(context, gateway) + storePushGateway(gateway) onDoneRunnable?.run() return@launch } thread.start() } - fun getExternalDistributors(context: Context): List { - val distributors = up.getDistributors(context).toMutableList() + fun getExternalDistributors(): List { + val distributors = up.getDistributors(context).toMutableList() distributors.remove(context.packageName) return distributors } - fun getCurrentDistributorName(context: Context): String { - if (isEmbeddedDistributor(context)) { - return context.getString(R.string.unifiedpush_distributor_fcm_fallback) + fun getCurrentDistributorName(): String { + if (isEmbeddedDistributor()) { + return stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) } - if (isBackgroundSync(context)) { - return context.getString(R.string.unifiedpush_distributor_background_sync) + if (isBackgroundSync()) { + return stringProvider.getString(R.string.unifiedpush_distributor_background_sync) } val distributor = up.getDistributor(context) return try { @@ -279,20 +285,20 @@ object UnifiedPushHelper { } as String } - fun isEmbeddedDistributor(context: Context): Boolean { + fun isEmbeddedDistributor(): Boolean { return (up.getDistributor(context) == context.packageName && FcmHelper.isPushSupported()) } - fun isBackgroundSync(context: Context): Boolean { + fun isBackgroundSync(): Boolean { return (up.getDistributor(context) == context.packageName && !FcmHelper.isPushSupported()) } - fun getPrivacyFriendlyUpEndpoint(context: Context): String? { - val endpoint = getEndpointOrToken(context) + fun getPrivacyFriendlyUpEndpoint(): String? { + val endpoint = getEndpointOrToken() if (endpoint.isNullOrEmpty()) return endpoint - if (isEmbeddedDistributor(context)) { + if (isEmbeddedDistributor()) { return endpoint } return try { diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 729c0bc435..d4b9395782 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -85,6 +85,7 @@ class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var vectorDataStore: VectorDataStore @Inject lateinit var wifiDetector: WifiDetector @Inject lateinit var guardServiceStarter: GuardServiceStarter + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -116,7 +117,7 @@ class VectorMessagingReceiver : MessagingReceiver() { .build() lateinit var notification: Notification - if (UnifiedPushHelper.isEmbeddedDistributor(context)) { + if (unifiedPushHelper.isEmbeddedDistributor()) { notification = moshi.adapter(Notification::class.java) .fromJson(sMessage) ?: return } else { @@ -159,10 +160,10 @@ class VectorMessagingReceiver : MessagingReceiver() { if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { // If the endpoint has changed // or the gateway has changed - if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint) { - UnifiedPushHelper.storeUpEndpoint(context, endpoint) - UnifiedPushHelper.storeCustomOrDefaultGateway(context, endpoint) { - UnifiedPushHelper.getPushGateway(context)?.let { + if (unifiedPushHelper.getEndpointOrToken() != endpoint) { + unifiedPushHelper.storeUpEndpoint(endpoint) + unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { + unifiedPushHelper.getPushGateway()?.let { pushersManager.enqueueRegisterPusher(endpoint, it) } } @@ -189,7 +190,7 @@ class VectorMessagingReceiver : MessagingReceiver() { guardServiceStarter.start() runBlocking { try { - pushersManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") + pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken() ?: "") } catch (e: Exception) { Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index cf532ea744..db03e7dc5d 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -72,7 +72,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP) class WebRtcCallManager @Inject constructor( private val context: Context, private val activeSessionDataSource: ActiveSessionDataSource, - private val analyticsTracker: AnalyticsTracker + private val analyticsTracker: AnalyticsTracker, + private val unifiedPushHelper: UnifiedPushHelper, ) : CallListener, DefaultLifecycleObserver { @@ -272,7 +273,7 @@ class WebRtcCallManager @Inject constructor( audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it if (isInBackground) { - if (!UnifiedPushHelper.isBackgroundSync(context)) { + if (!unifiedPushHelper.isBackgroundSync()) { currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing @@ -378,7 +379,7 @@ class WebRtcCallManager @Inject constructor( // and thus won't be able to received events. For example if the call is // accepted on an other session this device will continue ringing if (isInBackground) { - if (!UnifiedPushHelper.isBackgroundSync(context)) { + if (!unifiedPushHelper.isBackgroundSync()) { // only for push version as fdroid version is already doing it? currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index d1d1e2e4ce..dd99f6afe7 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -128,6 +128,7 @@ class HomeActivity : @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var appStateHandler: AppStateHandler + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { @@ -188,8 +189,8 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - UnifiedPushHelper.register(this) { - if (UnifiedPushHelper.isEmbeddedDistributor(this)) { + unifiedPushHelper.register { + if (unifiedPushHelper.isEmbeddedDistributor()) { FcmHelper.ensureFcmTokenIsRetrieved( this, pushManager, diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 282e62d43d..27b2b727a9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -63,6 +63,7 @@ import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml class VectorSettingsNotificationPreferenceFragment @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, @@ -99,10 +100,9 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - UnifiedPushHelper.register(requireContext()) + unifiedPushHelper.register() } else { - UnifiedPushHelper.unregister( - requireContext(), + unifiedPushHelper.unregister( pushersManager, vectorPreferences ) @@ -152,8 +152,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - UnifiedPushHelper.reRegister( - requireContext(), + unifiedPushHelper.reRegister( pushersManager, vectorPreferences ) { @@ -241,7 +240,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } findPreference(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { - it.isVisible = UnifiedPushHelper.isBackgroundSync(requireContext()) + it.isVisible = unifiedPushHelper.isBackgroundSync() } val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled() @@ -350,7 +349,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private fun refreshPref() { // This pref may have change from troubleshoot pref fragment - if (UnifiedPushHelper.isBackgroundSync(requireContext())) { + if (unifiedPushHelper.isBackgroundSync()) { findPreference(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) ?.isChecked = vectorPreferences.autoStartOnBoot() } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt index 9e25b27d0f..cc529d64a0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -18,19 +18,19 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.resources.StringProvider import im.vector.app.push.fcm.FcmHelper import javax.inject.Inject -class TestAvailableUnifiedPushDistributors @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider) : - TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { +class TestAvailableUnifiedPushDistributors @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + private val stringProvider: StringProvider, +) : TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - val distributors = UnifiedPushHelper.getExternalDistributors(context) + val distributors = unifiedPushHelper.getExternalDistributors() if (distributors.isEmpty()) { description = if (FcmHelper.isPushSupported()) { stringProvider.getString(R.string.settings_troubleshoot_test_distributors_gplay) @@ -39,8 +39,10 @@ class TestAvailableUnifiedPushDistributors @Inject constructor(private val conte } status = TestStatus.SUCCESS } else { - description = stringProvider.getString(R.string.settings_troubleshoot_test_distributors_many, - distributors.size + 1) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_distributors_many, + distributors.size + 1 + ) status = TestStatus.SUCCESS } } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt index 5eda48ffdc..d43fb1bfe3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt @@ -18,19 +18,21 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.resources.StringProvider import javax.inject.Inject -class TestCurrentUnifiedPushDistributor @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider) : - TroubleshootTest(R.string.settings_troubleshoot_test_current_distributor_title) { +class TestCurrentUnifiedPushDistributor @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + private val stringProvider: StringProvider, +) : TroubleshootTest(R.string.settings_troubleshoot_test_current_distributor_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_current_distributor, - UnifiedPushHelper.getCurrentDistributorName(context)) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_current_distributor, + unifiedPushHelper.getCurrentDistributorName() + ) status = TestStatus.SUCCESS } } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index 558b0e4fd9..9df4ca5298 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -31,16 +31,18 @@ import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject -class TestEndpointAsTokenRegistration @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider, - private val pushersManager: PushersManager, - private val vectorPreferences: VectorPreferences, - private val activeSessionHolder: ActiveSessionHolder) : - TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { +class TestEndpointAsTokenRegistration @Inject constructor( + private val context: FragmentActivity, + private val stringProvider: StringProvider, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, +) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Check if we have a registered pusher for this token - val endpoint = UnifiedPushHelper.getEndpointOrToken(context) ?: run { + val endpoint = unifiedPushHelper.getEndpointOrToken() ?: run { status = TestStatus.FAILED return } @@ -52,12 +54,13 @@ class TestEndpointAsTokenRegistration @Inject constructor(private val context: F it.pushKey == endpoint && it.state == PusherState.REGISTERED } if (pushers.isEmpty()) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_failed, - stringProvider.getString(R.string.sas_error_unknown)) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_endpoint_registration_failed, + stringProvider.getString(R.string.sas_error_unknown) + ) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { override fun doFix() { - UnifiedPushHelper.reRegister( - context, + unifiedPushHelper.reRegister( pushersManager, vectorPreferences ) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt index e8696eafc7..cf2bf3d5f1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -17,7 +17,6 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter @@ -34,12 +33,12 @@ import javax.inject.Inject /** * Test Push by asking the Push Gateway to send a Push back. */ -class TestPushFromPushGateway @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter, - private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder) : - TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { +class TestPushFromPushGateway @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter, + private val pushersManager: PushersManager, + private val activeSessionHolder: ActiveSessionHolder, +) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null private var pushReceived: Boolean = false @@ -47,7 +46,7 @@ class TestPushFromPushGateway @Inject constructor(private val context: FragmentA override fun perform(activityResultLauncher: ActivityResultLauncher) { pushReceived = false action = activeSessionHolder.getActiveSession().coroutineScope.launch { - val result = runCatching { pushersManager.testPush(context) } + val result = runCatching { pushersManager.testPush() } withContext(Dispatchers.Main) { status = result diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt index ab4e3dd7c2..fefb1d6478 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt @@ -18,21 +18,23 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.resources.StringProvider import javax.inject.Inject -class TestUnifiedPushEndpoint @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider) : - TroubleshootTest(R.string.settings_troubleshoot_test_current_endpoint_title) { +class TestUnifiedPushEndpoint @Inject constructor( + private val stringProvider: StringProvider, + private val unifiedPushHelper: UnifiedPushHelper, +) : TroubleshootTest(R.string.settings_troubleshoot_test_current_endpoint_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - val endpoint = UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context) + val endpoint = unifiedPushHelper.getPrivacyFriendlyUpEndpoint() endpoint?.let { - description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_success, - UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context)) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_current_endpoint_success, + unifiedPushHelper.getPrivacyFriendlyUpEndpoint() + ) status = TestStatus.SUCCESS } ?: run { description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_failed) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt index 75e8962c84..19a4fd188f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt @@ -18,19 +18,21 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.resources.StringProvider import javax.inject.Inject -class TestUnifiedPushGateway @Inject constructor(private val context: FragmentActivity, - private val stringProvider: StringProvider) : - TroubleshootTest(R.string.settings_troubleshoot_test_current_gateway_title) { +class TestUnifiedPushGateway @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, + private val stringProvider: StringProvider +) : TroubleshootTest(R.string.settings_troubleshoot_test_current_gateway_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_current_gateway, - UnifiedPushHelper.getPushGateway(context)) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_current_gateway, + unifiedPushHelper.getPushGateway() + ) status = TestStatus.SUCCESS } } From 5e10449746c2ca3cc5beea8309e713b7c358e046 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 15:59:17 +0200 Subject: [PATCH 030/314] Use the RawService to do network request. --- .../app/core/pushers/UnifiedPushHelper.kt | 54 +++++++------------ .../core/pushers/VectorMessagingReceiver.kt | 8 +-- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 20cad9e6ca..e2f494525b 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -22,8 +22,6 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences @@ -31,12 +29,10 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import okhttp3.OkHttpClient -import okhttp3.Request +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.util.MatrixJsonParser import org.unifiedpush.android.connector.UnifiedPush import timber.log.Timber import java.net.URL @@ -45,6 +41,7 @@ import javax.inject.Inject class UnifiedPushHelper @Inject constructor( private val context: Context, private val stringProvider: StringProvider, + private val matrix: Matrix, ) { companion object { private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" @@ -214,7 +211,7 @@ class UnifiedPushHelper @Inject constructor( val gateway: String = "" ) - fun storeCustomOrDefaultGateway( + suspend fun storeCustomOrDefaultGateway( endpoint: String, onDoneRunnable: Runnable? = null ) { @@ -233,34 +230,23 @@ class UnifiedPushHelper @Inject constructor( val parsed = URL(endpoint) val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" Timber.i("Testing $custom") - val thread = CoroutineScope(SupervisorJob()).launch { - try { - val moshi: Moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - val client = OkHttpClient() - val request = Request.Builder() - .url(custom) - .build() - val sResponse = client.newCall(request).execute() - .body?.string() ?: "" - moshi.adapter(DiscoveryResponse::class.java) - .fromJson(sResponse)?.let { response -> - if (response.unifiedpush.gateway == "matrix") { - Timber.d("Using custom gateway") - storePushGateway(custom) - onDoneRunnable?.run() - return@launch - } + try { + val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache) + val moshi = MatrixJsonParser.getMoshi() + moshi.adapter(DiscoveryResponse::class.java).fromJson(response) + ?.let { discoveryResponse -> + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + storePushGateway(custom) + onDoneRunnable?.run() + return } - } catch (e: Exception) { - Timber.d("Cannot try custom gateway: $e") - } - storePushGateway(gateway) - onDoneRunnable?.run() - return@launch + } + } catch (e: Exception) { + Timber.d("Cannot try custom gateway: $e") } - thread.start() + storePushGateway(gateway) + onDoneRunnable?.run() } fun getExternalDistributors(): List { diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index d4b9395782..8b8bb17765 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -162,9 +162,11 @@ class VectorMessagingReceiver : MessagingReceiver() { // or the gateway has changed if (unifiedPushHelper.getEndpointOrToken() != endpoint) { unifiedPushHelper.storeUpEndpoint(endpoint) - unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { - unifiedPushHelper.getPushGateway()?.let { - pushersManager.enqueueRegisterPusher(endpoint, it) + coroutineScope.launch { + unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { + unifiedPushHelper.getPushGateway()?.let { + pushersManager.enqueueRegisterPusher(endpoint, it) + } } } } else { From f1e57d2970b5b0e384ea57039fdb1928b278b21b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:01:11 +0200 Subject: [PATCH 031/314] Use `.orEmpty()` instead of `?: ""` --- .../src/main/java/im/vector/app/core/pushers/PushersManager.kt | 2 +- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- .../java/im/vector/app/core/pushers/VectorMessagingReceiver.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index cb3d7dfe8e..e935663f8b 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -41,7 +41,7 @@ class PushersManager @Inject constructor( currentSession.pushersService().testPush( unifiedPushHelper.getPushGateway()!!, stringProvider.getString(R.string.pusher_app_id), - unifiedPushHelper.getEndpointOrToken() ?: "", + unifiedPushHelper.getEndpointOrToken().orEmpty(), TEST_EVENT_ID ) } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index e2f494525b..31f6eb1285 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -191,7 +191,7 @@ class UnifiedPushHelper @Inject constructor( vectorPreferences?.setFdroidSyncBackgroundMode(mode) runBlocking { try { - pushersManager?.unregisterPusher(getEndpointOrToken() ?: "") + pushersManager?.unregisterPusher(getEndpointOrToken().orEmpty()) } catch (e: Exception) { Timber.d("Probably unregistering a non existant pusher") } diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 8b8bb17765..75d07c0988 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -192,7 +192,7 @@ class VectorMessagingReceiver : MessagingReceiver() { guardServiceStarter.start() runBlocking { try { - pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken() ?: "") + pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken().orEmpty()) } catch (e: Exception) { Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") } From 9216d8ba327557eab28f4fac502a71f4947d9f9a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:02:28 +0200 Subject: [PATCH 032/314] Small cleanup --- .../app/core/pushers/UnifiedPushHelper.kt | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 31f6eb1285..57810b659b 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -18,7 +18,6 @@ package im.vector.app.core.pushers import android.content.Context import android.content.pm.PackageManager -import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.JsonClass @@ -149,9 +148,6 @@ class UnifiedPushHelper @Inject constructor( up.registerApp(context) onDoneRunnable?.run() } else { - val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(context) - builder.setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) - val distributorsArray = distributors.toTypedArray() val distributorsNameArray = distributorsArray.map { if (it == context.packageName) { @@ -165,21 +161,23 @@ class UnifiedPushHelper @Inject constructor( } as String } }.toTypedArray() - builder.setItems(distributorsNameArray) { _, which -> - val distributor = distributorsArray[which] - up.saveDistributor(context, distributor) - Timber.i("Saving distributor: $distributor") - up.registerApp(context) - onDoneRunnable?.run() - } - builder.setOnDismissListener { - onDoneRunnable?.run() - } - builder.setOnCancelListener { - onDoneRunnable?.run() - } - val dialog: AlertDialog = builder.create() - dialog.show() + + MaterialAlertDialogBuilder(context) + .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) + .setItems(distributorsNameArray) { _, which -> + val distributor = distributorsArray[which] + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) + onDoneRunnable?.run() + } + .setOnDismissListener { + onDoneRunnable?.run() + } + .setOnCancelListener { + onDoneRunnable?.run() + } + .show() } } From 74de9c82c0db83822e884985915d738e6609fb43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:07:18 +0200 Subject: [PATCH 033/314] Small rework --- .../app/core/pushers/UnifiedPushHelper.kt | 20 ++++++++--------- .../TestAvailableUnifiedPushDistributors.kt | 22 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 57810b659b..11b71493fe 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -136,11 +136,13 @@ class UnifiedPushHelper @Inject constructor( up.saveDistributor(context, context.packageName) val distributors = up.getDistributors(context).toMutableList() - val internalDistributorName = if (FcmHelper.isPushSupported()) { - stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) - } else { - stringProvider.getString(R.string.unifiedpush_distributor_background_sync) - } + val internalDistributorName = stringProvider.getString( + if (FcmHelper.isPushSupported()) { + R.string.unifiedpush_distributor_fcm_fallback + } else { + R.string.unifiedpush_distributor_background_sync + } + ) if (distributors.size == 1 && !force) { @@ -217,11 +219,9 @@ class UnifiedPushHelper @Inject constructor( // register app_id type upfcm on sygnal // the pushkey if FCM key if (up.getDistributor(context) == context.packageName) { - stringProvider.getString(R.string.pusher_http_url).let { - storePushGateway(it) - onDoneRunnable?.run() - return - } + storePushGateway(stringProvider.getString(R.string.pusher_http_url)) + onDoneRunnable?.run() + return } // else, unifiedpush, and pushkey is an endpoint val gateway = stringProvider.getString(R.string.default_push_gateway_http_url) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt index cc529d64a0..9eb8cd35c4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -31,19 +31,17 @@ class TestAvailableUnifiedPushDistributors @Inject constructor( override fun perform(activityResultLauncher: ActivityResultLauncher) { val distributors = unifiedPushHelper.getExternalDistributors() - if (distributors.isEmpty()) { - description = if (FcmHelper.isPushSupported()) { - stringProvider.getString(R.string.settings_troubleshoot_test_distributors_gplay) - } else { - stringProvider.getString(R.string.settings_troubleshoot_test_distributors_fdroid) - } - status = TestStatus.SUCCESS - } else { - description = stringProvider.getString( - R.string.settings_troubleshoot_test_distributors_many, - distributors.size + 1 + description = if (distributors.isEmpty()) { + stringProvider.getString( + if (FcmHelper.isPushSupported()) { + R.string.settings_troubleshoot_test_distributors_gplay + } else { + R.string.settings_troubleshoot_test_distributors_fdroid + } ) - status = TestStatus.SUCCESS + } else { + stringProvider.getString(R.string.settings_troubleshoot_test_distributors_many, distributors.size + 1) } + status = TestStatus.SUCCESS } } From 12d969b2c0720e2b118f136411f8f7ac15887520 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:07:52 +0200 Subject: [PATCH 034/314] Prefer using `toString()` --- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 11b71493fe..31f544ec20 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -263,10 +263,10 @@ class UnifiedPushHelper @Inject constructor( val distributor = up.getDistributor(context) return try { val ai = context.packageManager.getApplicationInfo(distributor, 0) - context.packageManager.getApplicationLabel(ai) + context.packageManager.getApplicationLabel(ai).toString() } catch (e: PackageManager.NameNotFoundException) { distributor - } as String + } } fun isEmbeddedDistributor(): Boolean { From cc80bf986cdee42f80aeffbb41af2f4afce82641 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:10:12 +0200 Subject: [PATCH 035/314] Use plurals --- .../troubleshoot/TestAvailableUnifiedPushDistributors.kt | 3 ++- vector/src/main/res/values/strings.xml | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt index 9eb8cd35c4..c51aa22210 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -40,7 +40,8 @@ class TestAvailableUnifiedPushDistributors @Inject constructor( } ) } else { - stringProvider.getString(R.string.settings_troubleshoot_test_distributors_many, distributors.size + 1) + val quantity = distributors.size + 1 + stringProvider.getQuantityString(R.plurals.settings_troubleshoot_test_distributors_many, quantity, quantity) } status = TestStatus.SUCCESS } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 89cbefd859..e8dbdfb644 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3077,7 +3077,10 @@ Available methods No other method than Google Play Service found. No other method than background synchronization found. - Found %d methods. + + Found %d method. + Found %d methods. + Method Currently using %s. Endpoint From 399e95a247f5083ed717f5cb1a7bef5e460abbf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:11:14 +0200 Subject: [PATCH 036/314] `setOnDismissListener` should cover all the cases. --- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 31f544ec20..4e3004b76f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -171,14 +171,10 @@ class UnifiedPushHelper @Inject constructor( up.saveDistributor(context, distributor) Timber.i("Saving distributor: $distributor") up.registerApp(context) - onDoneRunnable?.run() } .setOnDismissListener { onDoneRunnable?.run() } - .setOnCancelListener { - onDoneRunnable?.run() - } .show() } } From ddf6a69a698ef6ef91d35f92b7e3d95f449a7fa1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:14:36 +0200 Subject: [PATCH 037/314] Small cleanup --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 4e3004b76f..5ceeaf9430 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -144,8 +144,7 @@ class UnifiedPushHelper @Inject constructor( } ) - if (distributors.size == 1 && - !force) { + if (distributors.size == 1 && !force) { up.saveDistributor(context, distributors.first()) up.registerApp(context) onDoneRunnable?.run() @@ -160,7 +159,7 @@ class UnifiedPushHelper @Inject constructor( context.packageManager.getApplicationLabel(ai) } catch (e: PackageManager.NameNotFoundException) { it - } as String + } } }.toTypedArray() From ad8cb22863a1513e37341b0f8f2203908209d488 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:24:13 +0200 Subject: [PATCH 038/314] We need an Activity to display the dialog --- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 12 ++++++++++-- .../java/im/vector/app/features/home/HomeActivity.kt | 2 +- .../VectorSettingsNotificationPreferenceFragment.kt | 3 ++- .../troubleshoot/TestEndpointAsTokenRegistration.kt | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 5ceeaf9430..d4af90ca0a 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -19,6 +19,7 @@ package im.vector.app.core.pushers import android.content.Context import android.content.pm.PackageManager import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.JsonClass import im.vector.app.BuildConfig @@ -91,18 +92,24 @@ class UnifiedPushHelper @Inject constructor( } } - fun register(onDoneRunnable: Runnable? = null) { + fun register( + activity: FragmentActivity, + onDoneRunnable: Runnable? = null, + ) { gRegister( + activity, onDoneRunnable = onDoneRunnable ) } fun reRegister( + activity: FragmentActivity, pushersManager: PushersManager, vectorPreferences: VectorPreferences, onDoneRunnable: Runnable? = null ) { gRegister( + activity, force = true, pushersManager = pushersManager, vectorPreferences = vectorPreferences, @@ -111,6 +118,7 @@ class UnifiedPushHelper @Inject constructor( } private fun gRegister( + activity: FragmentActivity, force: Boolean = false, pushersManager: PushersManager? = null, vectorPreferences: VectorPreferences? = null, @@ -163,7 +171,7 @@ class UnifiedPushHelper @Inject constructor( } }.toTypedArray() - MaterialAlertDialogBuilder(context) + MaterialAlertDialogBuilder(activity) .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setItems(distributorsNameArray) { _, which -> val distributor = distributorsArray[which] diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index dd99f6afe7..26bb72b26f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -189,7 +189,7 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - unifiedPushHelper.register { + unifiedPushHelper.register(this) { if (unifiedPushHelper.isEmbeddedDistributor()) { FcmHelper.ensureFcmTokenIsRetrieved( this, diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 27b2b727a9..0dcb0f37bc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -100,7 +100,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - unifiedPushHelper.register() + unifiedPushHelper.register(requireActivity()) } else { unifiedPushHelper.unregister( pushersManager, @@ -153,6 +153,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { unifiedPushHelper.reRegister( + requireActivity(), pushersManager, vectorPreferences ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index 9df4ca5298..c7c997ee82 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -61,6 +61,7 @@ class TestEndpointAsTokenRegistration @Inject constructor( quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { override fun doFix() { unifiedPushHelper.reRegister( + context, pushersManager, vectorPreferences ) From ff6aa1147c66d0a08ac834ec1e0ea3ff73d135eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:33:49 +0200 Subject: [PATCH 039/314] VectorPreferences can be injected. --- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 13 ++++--------- .../VectorSettingsNotificationPreferenceFragment.kt | 8 ++------ .../troubleshoot/TestEndpointAsTokenRegistration.kt | 5 +---- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index d4af90ca0a..7218f96679 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -41,6 +41,7 @@ import javax.inject.Inject class UnifiedPushHelper @Inject constructor( private val context: Context, private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, private val matrix: Matrix, ) { companion object { @@ -105,14 +106,12 @@ class UnifiedPushHelper @Inject constructor( fun reRegister( activity: FragmentActivity, pushersManager: PushersManager, - vectorPreferences: VectorPreferences, onDoneRunnable: Runnable? = null ) { gRegister( activity, force = true, pushersManager = pushersManager, - vectorPreferences = vectorPreferences, onDoneRunnable = onDoneRunnable ) } @@ -121,7 +120,6 @@ class UnifiedPushHelper @Inject constructor( activity: FragmentActivity, force: Boolean = false, pushersManager: PushersManager? = null, - vectorPreferences: VectorPreferences? = null, onDoneRunnable: Runnable? = null ) { if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { @@ -132,7 +130,7 @@ class UnifiedPushHelper @Inject constructor( } if (force) { // Un-register first - unregister(pushersManager, vectorPreferences) + unregister(pushersManager) } if (up.getDistributor(context).isNotEmpty()) { up.registerApp(context) @@ -186,12 +184,9 @@ class UnifiedPushHelper @Inject constructor( } } - fun unregister( - pushersManager: PushersManager? = null, - vectorPreferences: VectorPreferences? = null - ) { + fun unregister(pushersManager: PushersManager? = null) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME - vectorPreferences?.setFdroidSyncBackgroundMode(mode) + vectorPreferences.setFdroidSyncBackgroundMode(mode) runBlocking { try { pushersManager?.unregisterPusher(getEndpointOrToken().orEmpty()) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 0dcb0f37bc..3ded282838 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -102,10 +102,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( if (isChecked) { unifiedPushHelper.register(requireActivity()) } else { - unifiedPushHelper.unregister( - pushersManager, - vectorPreferences - ) + unifiedPushHelper.unregister(pushersManager) session.pushersService().refreshPushers() } } @@ -154,8 +151,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( it.onPreferenceClickListener = Preference.OnPreferenceClickListener { unifiedPushHelper.reRegister( requireActivity(), - pushersManager, - vectorPreferences + pushersManager ) { session.pushersService().refreshPushers() refreshBackgroundSyncPrefs() diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index c7c997ee82..5cacb8d6d6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -27,7 +27,6 @@ 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.core.resources.StringProvider -import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject @@ -35,7 +34,6 @@ class TestEndpointAsTokenRegistration @Inject constructor( private val context: FragmentActivity, private val stringProvider: StringProvider, private val pushersManager: PushersManager, - private val vectorPreferences: VectorPreferences, private val activeSessionHolder: ActiveSessionHolder, private val unifiedPushHelper: UnifiedPushHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { @@ -62,8 +60,7 @@ class TestEndpointAsTokenRegistration @Inject constructor( override fun doFix() { unifiedPushHelper.reRegister( context, - pushersManager, - vectorPreferences + pushersManager ) val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint) WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> From 77601f61fbae5c4d62b5b48c370dcc43f9f6634a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:34:21 +0200 Subject: [PATCH 040/314] typo --- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- .../java/im/vector/app/core/pushers/VectorMessagingReceiver.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 7218f96679..fa1c258bb4 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -191,7 +191,7 @@ class UnifiedPushHelper @Inject constructor( try { pushersManager?.unregisterPusher(getEndpointOrToken().orEmpty()) } catch (e: Exception) { - Timber.d("Probably unregistering a non existant pusher") + Timber.d("Probably unregistering a non existing pusher") } } storeUpEndpoint(null) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 75d07c0988..bf46609a31 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -194,7 +194,7 @@ class VectorMessagingReceiver : MessagingReceiver() { try { pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken().orEmpty()) } catch (e: Exception) { - Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") + Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher") } } } From 4018113c888c7cbb064d9a37ef9713dada82a8d6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:40:50 +0200 Subject: [PATCH 041/314] Better usage of Timber. --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index fa1c258bb4..3fcd252760 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -191,7 +191,7 @@ class UnifiedPushHelper @Inject constructor( try { pushersManager?.unregisterPusher(getEndpointOrToken().orEmpty()) } catch (e: Exception) { - Timber.d("Probably unregistering a non existing pusher") + Timber.d(e, "Probably unregistering a non existing pusher") } } storeUpEndpoint(null) @@ -239,7 +239,7 @@ class UnifiedPushHelper @Inject constructor( } } } catch (e: Exception) { - Timber.d("Cannot try custom gateway: $e") + Timber.d(e, "Cannot try custom gateway") } storePushGateway(gateway) onDoneRunnable?.run() @@ -287,7 +287,7 @@ class UnifiedPushHelper @Inject constructor( val parsed = URL(endpoint) "${parsed.protocol}://${parsed.host}/***" } catch (e: Exception) { - Timber.e("Error parsing unifiedpush endpoint: $e") + Timber.e(e, "Error parsing unifiedpush endpoint") null } } From fb8408c3da2b99ab1d6044f214893d76d5730499 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 16:45:02 +0200 Subject: [PATCH 042/314] Small cleanup --- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 3fcd252760..312cd866f8 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -246,9 +246,8 @@ class UnifiedPushHelper @Inject constructor( } fun getExternalDistributors(): List { - val distributors = up.getDistributors(context).toMutableList() - distributors.remove(context.packageName) - return distributors + return up.getDistributors(context) + .filterNot { it == context.packageName } } fun getCurrentDistributorName(): String { @@ -268,13 +267,11 @@ class UnifiedPushHelper @Inject constructor( } fun isEmbeddedDistributor(): Boolean { - return (up.getDistributor(context) == context.packageName && - FcmHelper.isPushSupported()) + return up.getDistributor(context) == context.packageName && FcmHelper.isPushSupported() } fun isBackgroundSync(): Boolean { - return (up.getDistributor(context) == context.packageName && - !FcmHelper.isPushSupported()) + return up.getDistributor(context) == context.packageName && !FcmHelper.isPushSupported() } fun getPrivacyFriendlyUpEndpoint(): String? { From 76bc6a5e0a2121250b11b84e75447d1fca693247 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 17:21:39 +0200 Subject: [PATCH 043/314] Move the setting at the beginning of the section --- .../main/res/xml/vector_settings_notifications.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index c8434b6920..c331f056d1 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -51,6 +51,11 @@ android:persistent="false" android:title="@string/settings_notification_configuration"> + + - - - \ No newline at end of file + From 3f6b5292d4b345b4a0a7460cc7163e02579bd77f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 17:28:01 +0200 Subject: [PATCH 044/314] Add summary to the notification with the current value. --- .../VectorSettingsNotificationPreferenceFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 3ded282838..f8feb57c72 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -148,11 +148,13 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { unifiedPushHelper.reRegister( requireActivity(), pushersManager ) { + it.summary = unifiedPushHelper.getCurrentDistributorName() session.pushersService().refreshPushers() refreshBackgroundSyncPrefs() } From bdb2d29666b44ee80b17d9c4aa5f5bca379fda28 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 17:46:55 +0200 Subject: [PATCH 045/314] Catch 404 --- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 312cd866f8..6ca47c82a0 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -238,7 +238,7 @@ class UnifiedPushHelper @Inject constructor( return } } - } catch (e: Exception) { + } catch (e: Throwable) { Timber.d(e, "Cannot try custom gateway") } storePushGateway(gateway) From bbbeb4b283a5958daf5712f769fffa6f217f4224 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 17:57:47 +0200 Subject: [PATCH 046/314] Extract storage to its own class. --- .../vector/app/core/pushers/PushersManager.kt | 6 +- .../app/core/pushers/UnifiedPushHelper.kt | 64 ++-------------- .../app/core/pushers/UnifiedPushStore.kt | 73 +++++++++++++++++++ .../core/pushers/VectorMessagingReceiver.kt | 9 ++- .../TestEndpointAsTokenRegistration.kt | 4 +- .../troubleshoot/TestUnifiedPushGateway.kt | 6 +- 6 files changed, 95 insertions(+), 67 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index e935663f8b..cb261f8288 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -29,7 +29,7 @@ import kotlin.math.abs private const val DEFAULT_PUSHER_FILE_TAG = "mobile" class PushersManager @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushStore: UnifiedPushStore, private val activeSessionHolder: ActiveSessionHolder, private val localeProvider: LocaleProvider, private val stringProvider: StringProvider, @@ -39,9 +39,9 @@ class PushersManager @Inject constructor( val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - unifiedPushHelper.getPushGateway()!!, + unifiedPushStore.getPushGateway()!!, stringProvider.getString(R.string.pusher_app_id), - unifiedPushHelper.getEndpointOrToken().orEmpty(), + unifiedPushStore.getEndpointOrToken().orEmpty(), TEST_EVENT_ID ) } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 6ca47c82a0..18fb28df7e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -18,13 +18,11 @@ package im.vector.app.core.pushers import android.content.Context import android.content.pm.PackageManager -import androidx.core.content.edit import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.JsonClass import im.vector.app.BuildConfig import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences @@ -40,59 +38,13 @@ import javax.inject.Inject class UnifiedPushHelper @Inject constructor( private val context: Context, + private val unifiedPushStore: UnifiedPushStore, private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, private val matrix: Matrix, ) { - companion object { - private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" - private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" - } - private val up = UnifiedPush - /** - * Retrieves the UnifiedPush Endpoint. - * - * @return the UnifiedPush Endpoint or null if not received - */ - fun getEndpointOrToken(): String? { - return DefaultSharedPreferences.getInstance(context).getString(PREFS_ENDPOINT_OR_TOKEN, null) - } - - /** - * Store UnifiedPush Endpoint to the SharedPrefs. - * TODO Store in realm - * - * @param endpoint the endpoint to store - */ - fun storeUpEndpoint(endpoint: String?) { - DefaultSharedPreferences.getInstance(context).edit { - putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) - } - } - - /** - * Retrieves the Push Gateway. - * - * @return the Push Gateway or null if not defined - */ - fun getPushGateway(): String? { - return DefaultSharedPreferences.getInstance(context).getString(PREFS_PUSH_GATEWAY, null) - } - - /** - * Store Push Gateway to the SharedPrefs. - * TODO Store in realm - * - * @param gateway the push gateway to store - */ - private fun storePushGateway(gateway: String?) { - DefaultSharedPreferences.getInstance(context).edit { - putString(PREFS_PUSH_GATEWAY, gateway) - } - } - fun register( activity: FragmentActivity, onDoneRunnable: Runnable? = null, @@ -189,13 +141,13 @@ class UnifiedPushHelper @Inject constructor( vectorPreferences.setFdroidSyncBackgroundMode(mode) runBlocking { try { - pushersManager?.unregisterPusher(getEndpointOrToken().orEmpty()) + pushersManager?.unregisterPusher(unifiedPushStore.getEndpointOrToken().orEmpty()) } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") } } - storeUpEndpoint(null) - storePushGateway(null) + unifiedPushStore.storeUpEndpoint(null) + unifiedPushStore.storePushGateway(null) up.unregisterApp(context) } @@ -217,7 +169,7 @@ class UnifiedPushHelper @Inject constructor( // register app_id type upfcm on sygnal // the pushkey if FCM key if (up.getDistributor(context) == context.packageName) { - storePushGateway(stringProvider.getString(R.string.pusher_http_url)) + unifiedPushStore.storePushGateway(stringProvider.getString(R.string.pusher_http_url)) onDoneRunnable?.run() return } @@ -233,7 +185,7 @@ class UnifiedPushHelper @Inject constructor( ?.let { discoveryResponse -> if (discoveryResponse.unifiedpush.gateway == "matrix") { Timber.d("Using custom gateway") - storePushGateway(custom) + unifiedPushStore.storePushGateway(custom) onDoneRunnable?.run() return } @@ -241,7 +193,7 @@ class UnifiedPushHelper @Inject constructor( } catch (e: Throwable) { Timber.d(e, "Cannot try custom gateway") } - storePushGateway(gateway) + unifiedPushStore.storePushGateway(gateway) onDoneRunnable?.run() } @@ -275,7 +227,7 @@ class UnifiedPushHelper @Inject constructor( } fun getPrivacyFriendlyUpEndpoint(): String? { - val endpoint = getEndpointOrToken() + val endpoint = unifiedPushStore.getEndpointOrToken() if (endpoint.isNullOrEmpty()) return endpoint if (isEmbeddedDistributor()) { return endpoint diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt new file mode 100644 index 0000000000..05e1131c0b --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt @@ -0,0 +1,73 @@ +/* + * 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.pushers + +import android.content.Context +import androidx.core.content.edit +import im.vector.app.core.di.DefaultSharedPreferences +import javax.inject.Inject + +class UnifiedPushStore @Inject constructor( + context: Context, +) { + companion object { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + } + + private val defaultPrefs = DefaultSharedPreferences.getInstance(context) + + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpointOrToken(): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs. + * + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(endpoint: String?) { + defaultPrefs.edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(): String? { + return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null) + } + + /** + * Store Push Gateway to the SharedPrefs. + * + * @param gateway the push gateway to store + */ + fun storePushGateway(gateway: String?) { + defaultPrefs.edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index bf46609a31..98d759625a 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -86,6 +86,7 @@ class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var wifiDetector: WifiDetector @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var unifiedPushStore: UnifiedPushStore private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -160,11 +161,11 @@ class VectorMessagingReceiver : MessagingReceiver() { if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { // If the endpoint has changed // or the gateway has changed - if (unifiedPushHelper.getEndpointOrToken() != endpoint) { - unifiedPushHelper.storeUpEndpoint(endpoint) + if (unifiedPushStore.getEndpointOrToken() != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint) coroutineScope.launch { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { - unifiedPushHelper.getPushGateway()?.let { + unifiedPushStore.getPushGateway()?.let { pushersManager.enqueueRegisterPusher(endpoint, it) } } @@ -192,7 +193,7 @@ class VectorMessagingReceiver : MessagingReceiver() { guardServiceStarter.start() runBlocking { try { - pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken().orEmpty()) + pushersManager.unregisterPusher(unifiedPushStore.getEndpointOrToken().orEmpty()) } catch (e: Exception) { Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher") } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index 5cacb8d6d6..66222f759e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -26,6 +26,7 @@ import im.vector.app.R 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.core.pushers.UnifiedPushStore import im.vector.app.core.resources.StringProvider import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject @@ -36,11 +37,12 @@ class TestEndpointAsTokenRegistration @Inject constructor( private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushStore: UnifiedPushStore, ) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Check if we have a registered pusher for this token - val endpoint = unifiedPushHelper.getEndpointOrToken() ?: run { + val endpoint = unifiedPushStore.getEndpointOrToken() ?: run { status = TestStatus.FAILED return } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt index 19a4fd188f..38f14951b4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt @@ -19,19 +19,19 @@ package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher import im.vector.app.R -import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.pushers.UnifiedPushStore import im.vector.app.core.resources.StringProvider import javax.inject.Inject class TestUnifiedPushGateway @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushStore: UnifiedPushStore, private val stringProvider: StringProvider ) : TroubleshootTest(R.string.settings_troubleshoot_test_current_gateway_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { description = stringProvider.getString( R.string.settings_troubleshoot_test_current_gateway, - unifiedPushHelper.getPushGateway() + unifiedPushStore.getPushGateway() ) status = TestStatus.SUCCESS } From 45768c5251e5a482a2e032a3f283511112290811 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 18:15:50 +0200 Subject: [PATCH 047/314] Small cleanup --- .../core/pushers/VectorMessagingReceiver.kt | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 98d759625a..10335d0f75 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -26,8 +26,6 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder @@ -48,6 +46,7 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.api.util.MatrixJsonParser import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject @@ -113,20 +112,15 @@ class VectorMessagingReceiver : MessagingReceiver() { vectorDataStore.incrementPushCounter() } - val moshi: Moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - lateinit var notification: Notification - - if (unifiedPushHelper.isEmbeddedDistributor()) { - notification = moshi.adapter(Notification::class.java) - .fromJson(sMessage) ?: return + val moshi = MatrixJsonParser.getMoshi() + val notification: Notification = if (unifiedPushHelper.isEmbeddedDistributor()) { + moshi.adapter(Notification::class.java).fromJson(sMessage) } else { - val data = moshi.adapter(UnifiedPushMessage::class.java) - .fromJson(sMessage) ?: return - notification = data.notification - notification.unread = notification.counts.unread - } + val data = moshi.adapter(UnifiedPushMessage::class.java).fromJson(sMessage) + data?.notification?.also { + it.unread = it.counts.unread + } + } ?: return Unit.also { Timber.w("Invalid received data") } // Diagnostic Push if (notification.eventId == PushersManager.TEST_EVENT_ID) { From cdfb728a73b1609b168cc5aab49e889f75f4c2df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jun 2022 19:51:26 +0200 Subject: [PATCH 048/314] Clarify the data classes for the Json parsing --- .../core/pushers/VectorMessagingReceiver.kt | 49 +++++----------- .../vector/app/core/pushers/model/PushData.kt | 23 ++++++++ .../app/core/pushers/model/PushDataFcm.kt | 44 ++++++++++++++ .../core/pushers/model/PushDataUnifiedPush.kt | 58 +++++++++++++++++++ 4 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt create mode 100644 vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt create mode 100644 vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 10335d0f75..bb2f253abd 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -24,12 +24,14 @@ import android.widget.Toast import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector +import im.vector.app.core.pushers.model.PushData +import im.vector.app.core.pushers.model.PushDataFcm +import im.vector.app.core.pushers.model.PushDataUnifiedPush +import im.vector.app.core.pushers.model.toPushData import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager @@ -51,24 +53,6 @@ import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject -@JsonClass(generateAdapter = true) -data class UnifiedPushMessage( - val notification: Notification = Notification() -) - -@JsonClass(generateAdapter = true) -data class Notification( - @Json(name = "event_id") val eventId: String = "", - @Json(name = "room_id") val roomId: String = "", - var unread: Int = 0, - val counts: Counts = Counts() -) - -@JsonClass(generateAdapter = true) -data class Counts( - val unread: Int = 0 -) - private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) /** @@ -113,17 +97,14 @@ class VectorMessagingReceiver : MessagingReceiver() { } val moshi = MatrixJsonParser.getMoshi() - val notification: Notification = if (unifiedPushHelper.isEmbeddedDistributor()) { - moshi.adapter(Notification::class.java).fromJson(sMessage) + val pushData = if (unifiedPushHelper.isEmbeddedDistributor()) { + moshi.adapter(PushDataFcm::class.java).fromJson(sMessage)?.toPushData() } else { - val data = moshi.adapter(UnifiedPushMessage::class.java).fromJson(sMessage) - data?.notification?.also { - it.unread = it.counts.unread - } - } ?: return Unit.also { Timber.w("Invalid received data") } + moshi.adapter(PushDataUnifiedPush::class.java).fromJson(sMessage)?.toPushData() + } ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } // Diagnostic Push - if (notification.eventId == PushersManager.TEST_EVENT_ID) { + if (pushData.eventId == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) LocalBroadcastManager.getInstance(context).sendBroadcast(intent) return @@ -139,7 +120,7 @@ class VectorMessagingReceiver : MessagingReceiver() { // we are in foreground, let the sync do the things? Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { - onMessageReceivedInternal(notification) + onMessageReceivedInternal(pushData) } } } @@ -197,12 +178,12 @@ class VectorMessagingReceiver : MessagingReceiver() { /** * Internal receive method. * - * @param notification Notification containing message data. + * @param pushData Object containing message data. */ - private fun onMessageReceivedInternal(notification: Notification) { + private fun onMessageReceivedInternal(pushData: PushData) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $pushData") } else { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } @@ -212,12 +193,12 @@ class VectorMessagingReceiver : MessagingReceiver() { if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { - if (isEventAlreadyKnown(notification.eventId, notification.roomId)) { + if (isEventAlreadyKnown(pushData.eventId, pushData.roomId)) { Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, notification.roomId, notification.eventId) + getEventFastLane(session, pushData.roomId, pushData.eventId) Timber.tag(loggerTag.value).d("Requesting background sync") session.syncService().requireBackgroundSync() diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt new file mode 100644 index 0000000000..9f7b710c91 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt @@ -0,0 +1,23 @@ +/* + * 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.pushers.model + +data class PushData( + val eventId: String, + val roomId: String, + var unread: Int, +) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt new file mode 100644 index 0000000000..bf81377ccb --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt @@ -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.core.pushers.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * In this case, the format is: + *
+ * {
+ *     "event_id":"$anEventId",
+ *     "room_id":"!aRoomId",
+ *     "unread":"1",
+ *     "prio":"high",
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +data class PushDataFcm( + @Json(name = "event_id") val eventId: String = "", + @Json(name = "room_id") val roomId: String = "", + @Json(name = "unread") var unread: Int = 0, +) + +fun PushDataFcm.toPushData() = PushData( + eventId = eventId, + roomId = roomId, + unread = unread +) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt new file mode 100644 index 0000000000..3174165218 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -0,0 +1,58 @@ +/* + * 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.pushers.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * In this case, the format is: + *
+ * {
+ *     "notification":{
+ *         "event_id":"$anEventId",
+ *         "room_id":"!aRoomId",
+ *         "counts":{
+ *             "unread":1
+ *         },
+ *         "prio":"high",
+ *     }
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPush( + @Json(name = "notification") val notification: PushDataUnifiedPushNotification = PushDataUnifiedPushNotification() +) + +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPushNotification( + @Json(name = "event_id") val eventId: String = "", + @Json(name = "room_id") val roomId: String = "", + @Json(name = "counts") var counts: PushDataUnifiedPushCounts = PushDataUnifiedPushCounts(), +) + +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPushCounts( + @Json(name = "unread") val unread: Int = 0 +) + +fun PushDataUnifiedPush.toPushData() = PushData( + eventId = notification.eventId, + roomId = notification.roomId, + unread = notification.counts.unread +) From 6cc2a36ee1e57faaafa36c00df384ed22b0d7a04 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 10:14:49 +0200 Subject: [PATCH 049/314] Add explicit Json names --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 18fb28df7e..1f712fa1e0 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.pm.PackageManager import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.app.BuildConfig import im.vector.app.R @@ -153,12 +154,12 @@ class UnifiedPushHelper @Inject constructor( @JsonClass(generateAdapter = true) internal data class DiscoveryResponse( - val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() + @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() ) @JsonClass(generateAdapter = true) internal data class DiscoveryUnifiedPush( - val gateway: String = "" + @Json(name = "gateway") val gateway: String = "" ) suspend fun storeCustomOrDefaultGateway( From 35e0a0af3319d61ef0ff0f5e0a44f9af68c13579 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 10:58:11 +0200 Subject: [PATCH 050/314] Detekt --- .../main/java/im/vector/app/core/pushers/model/PushDataFcm.kt | 1 + .../java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt index bf81377ccb..b3bcc69309 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt @@ -29,6 +29,7 @@ import com.squareup.moshi.JsonClass * "prio":"high", * } * + * . */ @JsonClass(generateAdapter = true) data class PushDataFcm( diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt index 3174165218..b1410e048f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -33,6 +33,7 @@ import com.squareup.moshi.JsonClass * } * } * + * . */ @JsonClass(generateAdapter = true) data class PushDataUnifiedPush( From 80d42f0963cc1d981689b9ea99f0ee8df9fd1d8b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 11:37:12 +0200 Subject: [PATCH 051/314] Remove unused methods / clarify API --- .../vector/app/core/pushers/PushersManager.kt | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index cb261f8288..91ab58207d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -47,8 +47,7 @@ class PushersManager @Inject constructor( } fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { - val currentSession = activeSessionHolder.getActiveSession() - return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) + return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url)) } fun enqueueRegisterPusher( @@ -56,25 +55,13 @@ class PushersManager @Inject constructor( gateway: String ): UUID { val currentSession = activeSessionHolder.getActiveSession() - return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey, gateway)) - } - - suspend fun registerPusherWithFcmKey(pushKey: String) { - val currentSession = activeSessionHolder.getActiveSession() - currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) - } - - suspend fun registerPusher( - pushKey: String, - gateway: String - ) { - val currentSession = activeSessionHolder.getActiveSession() - currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey, gateway)) + val pusher = createHttpPusher(pushKey, gateway) + return currentSession.pushersService().enqueueAddHttpPusher(pusher) } private fun createHttpPusher( pushKey: String, - gateway: String = stringProvider.getString(R.string.pusher_http_url) + gateway: String ) = HttpPusher( pushKey, stringProvider.getString(R.string.pusher_app_id), From 18b49068c12add2812f981b01556ecc173be6a9d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 12:29:55 +0200 Subject: [PATCH 052/314] Change BuildConfig field to a VectorFeature. --- vector/build.gradle | 4 ---- .../debug/features/DebugFeaturesStateFactory.kt | 5 +++++ .../features/debug/features/DebugVectorFeatures.kt | 4 ++++ .../fcm/NotificationTroubleshootTestManagerFactory.kt | 7 ++++--- .../fcm/NotificationTroubleshootTestManagerFactory.kt | 7 ++++--- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 5 +++-- .../java/im/vector/app/features/VectorFeatures.kt | 11 +++++++++++ .../VectorSettingsNotificationPreferenceFragment.kt | 7 ++++--- 8 files changed, 35 insertions(+), 15 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index c0112a2934..82f43c6ac4 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -165,10 +165,6 @@ android { buildConfigField "Boolean", "enableLocationSharing", "true" buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\"" - // Set to false to prevent usage of UnifiedPush. For Gplay variant it means that only FCM will be used, - // And for F-Droid variant, it means that only background polling will be available to the user. - buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index aa4df5e308..248d9d232b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -65,6 +65,11 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.onboardingCombinedLogin, factory = VectorFeatures::isOnboardingCombinedLoginEnabled ), + createBooleanFeature( + label = "Allow external UnifiedPush distributors", + key = DebugFeatureKeys.allowExternalUnifiedPushDistributors, + factory = VectorFeatures::allowExternalUnifiedPushDistributors + ), ) ) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index f36b1a804a..919cc6635e 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -60,6 +60,9 @@ class DebugVectorFeatures( override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin) ?: vectorFeatures.isOnboardingCombinedLoginEnabled() + override fun allowExternalUnifiedPushDistributors(): Boolean = read(DebugFeatureKeys.allowExternalUnifiedPushDistributors) + ?: vectorFeatures.allowExternalUnifiedPushDistributors() + override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing) ?: vectorFeatures.isScreenSharingEnabled() @@ -117,6 +120,7 @@ object DebugFeatureKeys { val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize") val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register") val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login") + val allowExternalUnifiedPushDistributors = booleanPreferencesKey("allow-external-unified-push-distributors") val liveLocationSharing = booleanPreferencesKey("live-location-sharing") val screenSharing = booleanPreferencesKey("screen-sharing") } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index b049edaf9a..5873b4308f 100644 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -16,11 +16,11 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment -import im.vector.app.BuildConfig import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot import im.vector.app.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions import im.vector.app.fdroid.features.settings.troubleshoot.TestBatteryOptimization +import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TestAccountSettings import im.vector.app.features.settings.troubleshoot.TestAvailableUnifiedPushDistributors @@ -50,7 +50,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( private val testAutoStartBoot: TestAutoStartBoot, private val testBackgroundRestrictions: TestBackgroundRestrictions, private val testBatteryOptimization: TestBatteryOptimization, - private val testNotification: TestNotification + private val testNotification: TestNotification, + private val vectorFeatures: VectorFeatures, ) { fun create(fragment: Fragment): NotificationTroubleshootTestManager { @@ -59,7 +60,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testAccountSettings) mgr.addTest(testDeviceSettings) mgr.addTest(testPushRulesSettings) - if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + if (vectorFeatures.allowExternalUnifiedPushDistributors()) { mgr.addTest(testAvailableUnifiedPushDistributors) mgr.addTest(testCurrentUnifiedPushDistributor) } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 767078b0d6..b3425c778b 100644 --- a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -16,8 +16,8 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment -import im.vector.app.BuildConfig import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TestAccountSettings import im.vector.app.features.settings.troubleshoot.TestAvailableUnifiedPushDistributors @@ -50,7 +50,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( private val testAvailableUnifiedPushDistributors: TestAvailableUnifiedPushDistributors, private val testEndpointAsTokenRegistration: TestEndpointAsTokenRegistration, private val testPushFromPushGateway: TestPushFromPushGateway, - private val testNotification: TestNotification + private val testNotification: TestNotification, + private val vectorFeatures: VectorFeatures, ) { fun create(fragment: Fragment): NotificationTroubleshootTestManager { @@ -59,7 +60,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testAccountSettings) mgr.addTest(testDeviceSettings) mgr.addTest(testPushRulesSettings) - if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + if (vectorFeatures.allowExternalUnifiedPushDistributors()) { mgr.addTest(testAvailableUnifiedPushDistributors) mgr.addTest(testCurrentUnifiedPushDistributor) } diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 1f712fa1e0..f2d2e802c4 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -22,9 +22,9 @@ import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper @@ -43,6 +43,7 @@ class UnifiedPushHelper @Inject constructor( private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, private val matrix: Matrix, + private val vectorFeatures: VectorFeatures, ) { private val up = UnifiedPush @@ -75,7 +76,7 @@ class UnifiedPushHelper @Inject constructor( pushersManager: PushersManager? = null, onDoneRunnable: Runnable? = null ) { - if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { up.saveDistributor(context, context.packageName) up.registerApp(context) onDoneRunnable?.run() diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 6a7a0865de..85b04dfbdc 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -27,6 +27,7 @@ interface VectorFeatures { fun isOnboardingPersonalizeEnabled(): Boolean fun isOnboardingCombinedRegisterEnabled(): Boolean fun isOnboardingCombinedLoginEnabled(): Boolean + fun allowExternalUnifiedPushDistributors(): Boolean fun isScreenSharingEnabled(): Boolean enum class OnboardingVariant { @@ -44,5 +45,15 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false override fun isOnboardingCombinedLoginEnabled() = false + + /** + * Return false to prevent usage of external UnifiedPush distributors. + * - For Gplay variant it means that only FCM will be used; + * - For F-Droid variant, it means that only background polling will be available to the user. + * Return true to allow any available external UnifiedPush distributor to be chosen by the user. + * - For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; + * - For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. + */ + override fun allowExternalUnifiedPushDistributors(): Boolean = true override fun isScreenSharingEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index f8feb57c72..658dffab12 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -30,7 +30,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult @@ -44,6 +43,7 @@ import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.utils.combineLatest import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.requestDisablingBatteryOptimization +import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode @@ -67,7 +67,8 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, - private val guardServiceStarter: GuardServiceStarter + private val guardServiceStarter: GuardServiceStarter, + private val vectorFeatures: VectorFeatures, ) : VectorSettingsBaseFragment(), BackgroundSyncModeChooserDialog.InteractionListener { @@ -147,7 +148,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { - if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { unifiedPushHelper.reRegister( From 110c17e57da82130d349d3f6d1e666cd8e4f561e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 12:36:05 +0200 Subject: [PATCH 053/314] No need to have a mutable list here. --- .../main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index f2d2e802c4..a39dc6010e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -94,7 +94,7 @@ class UnifiedPushHelper @Inject constructor( // By default, use internal solution (fcm/background sync) up.saveDistributor(context, context.packageName) - val distributors = up.getDistributors(context).toMutableList() + val distributors = up.getDistributors(context) val internalDistributorName = stringProvider.getString( if (FcmHelper.isPushSupported()) { From 420144dcebca18a50c58bfb628ec201d392f0322 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 14:18:49 +0200 Subject: [PATCH 054/314] Fix back issue on the dialog. Should split UI a bit more. --- .../app/core/pushers/UnifiedPushHelper.kt | 90 ++++++++++++------- ...rSettingsNotificationPreferenceFragment.kt | 5 +- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index a39dc6010e..bf1d1cc5cc 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -96,6 +96,38 @@ class UnifiedPushHelper @Inject constructor( up.saveDistributor(context, context.packageName) val distributors = up.getDistributors(context) + if (distributors.size == 1 && !force) { + up.saveDistributor(context, distributors.first()) + up.registerApp(context) + onDoneRunnable?.run() + } else { + openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force) + } + } + + fun openDistributorDialog( + activity: FragmentActivity, + pushersManager: PushersManager, + onDoneRunnable: Runnable, + ) { + val distributors = up.getDistributors(activity) + openDistributorDialogInternal( + activity, + pushersManager, + onDoneRunnable, distributors, + unregisterFirst = true, + cancellable = true, + ) + } + + private fun openDistributorDialogInternal( + activity: FragmentActivity, + pushersManager: PushersManager?, + onDoneRunnable: Runnable?, + distributors: List, + unregisterFirst: Boolean, + cancellable: Boolean, + ) { val internalDistributorName = stringProvider.getString( if (FcmHelper.isPushSupported()) { R.string.unifiedpush_distributor_fcm_fallback @@ -104,38 +136,36 @@ class UnifiedPushHelper @Inject constructor( } ) - if (distributors.size == 1 && !force) { - up.saveDistributor(context, distributors.first()) - up.registerApp(context) - onDoneRunnable?.run() - } else { - val distributorsArray = distributors.toTypedArray() - val distributorsNameArray = distributorsArray.map { - if (it == context.packageName) { - internalDistributorName - } else { - try { - val ai = context.packageManager.getApplicationInfo(it, 0) - context.packageManager.getApplicationLabel(ai) - } catch (e: PackageManager.NameNotFoundException) { - it - } + val distributorsName = distributors.map { + if (it == context.packageName) { + internalDistributorName + } else { + try { + val ai = context.packageManager.getApplicationInfo(it, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it } - }.toTypedArray() - - MaterialAlertDialogBuilder(activity) - .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) - .setItems(distributorsNameArray) { _, which -> - val distributor = distributorsArray[which] - up.saveDistributor(context, distributor) - Timber.i("Saving distributor: $distributor") - up.registerApp(context) - } - .setOnDismissListener { - onDoneRunnable?.run() - } - .show() + } } + + MaterialAlertDialogBuilder(activity) + .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) + .setItems(distributorsName.toTypedArray()) { _, which -> + if (unregisterFirst) { + // Un-register first + unregister(pushersManager) + } + val distributor = distributors[which] + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) + } + .setCancelable(cancellable) + .setOnDismissListener { + onDoneRunnable?.run() + } + .show() } fun unregister(pushersManager: PushersManager? = null) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 658dffab12..47539dd7c3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -151,10 +151,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - unifiedPushHelper.reRegister( - requireActivity(), - pushersManager - ) { + unifiedPushHelper.openDistributorDialog(requireActivity(), pushersManager) { it.summary = unifiedPushHelper.getCurrentDistributorName() session.pushersService().refreshPushers() refreshBackgroundSyncPrefs() From fb7df5bf46c639cc7cf0b65d669a1877f46b7377 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 14:32:23 +0200 Subject: [PATCH 055/314] Ignore if no change is done. --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index bf1d1cc5cc..1ae095b6c0 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -152,11 +152,16 @@ class UnifiedPushHelper @Inject constructor( MaterialAlertDialogBuilder(activity) .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setItems(distributorsName.toTypedArray()) { _, which -> + val distributor = distributors[which] + if (distributor == getCurrentDistributorName()) { + Timber.d("Same distributor selected again, no action") + return@setItems + } + if (unregisterFirst) { // Un-register first unregister(pushersManager) } - val distributor = distributors[which] up.saveDistributor(context, distributor) Timber.i("Saving distributor: $distributor") up.registerApp(context) From a5378d6e943833b00fadacec4a036afbce2b3264 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 14:41:46 +0200 Subject: [PATCH 056/314] avoid runBlocking --- .../app/core/pushers/UnifiedPushHelper.kt | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 1ae095b6c0..e975f4cf57 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -19,6 +19,7 @@ package im.vector.app.core.pushers import android.content.Context import android.content.pm.PackageManager import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -28,7 +29,7 @@ import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.util.MatrixJsonParser @@ -76,32 +77,34 @@ class UnifiedPushHelper @Inject constructor( pushersManager: PushersManager? = null, onDoneRunnable: Runnable? = null ) { - if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { + activity.lifecycleScope.launch { + if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { + up.saveDistributor(context, context.packageName) + up.registerApp(context) + onDoneRunnable?.run() + return@launch + } + if (force) { + // Un-register first + unregister(pushersManager) + } + if (up.getDistributor(context).isNotEmpty()) { + up.registerApp(context) + onDoneRunnable?.run() + return@launch + } + + // By default, use internal solution (fcm/background sync) up.saveDistributor(context, context.packageName) - up.registerApp(context) - onDoneRunnable?.run() - return - } - if (force) { - // Un-register first - unregister(pushersManager) - } - if (up.getDistributor(context).isNotEmpty()) { - up.registerApp(context) - onDoneRunnable?.run() - return - } + val distributors = up.getDistributors(context) - // By default, use internal solution (fcm/background sync) - up.saveDistributor(context, context.packageName) - val distributors = up.getDistributors(context) - - if (distributors.size == 1 && !force) { - up.saveDistributor(context, distributors.first()) - up.registerApp(context) - onDoneRunnable?.run() - } else { - openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force) + if (distributors.size == 1 && !force) { + up.saveDistributor(context, distributors.first()) + up.registerApp(context) + onDoneRunnable?.run() + } else { + openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force) + } } } @@ -158,13 +161,15 @@ class UnifiedPushHelper @Inject constructor( return@setItems } - if (unregisterFirst) { - // Un-register first - unregister(pushersManager) + activity.lifecycleScope.launch { + if (unregisterFirst) { + // Un-register first + unregister(pushersManager) + } + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) } - up.saveDistributor(context, distributor) - Timber.i("Saving distributor: $distributor") - up.registerApp(context) } .setCancelable(cancellable) .setOnDismissListener { @@ -173,15 +178,13 @@ class UnifiedPushHelper @Inject constructor( .show() } - fun unregister(pushersManager: PushersManager? = null) { + suspend fun unregister(pushersManager: PushersManager? = null) { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME vectorPreferences.setFdroidSyncBackgroundMode(mode) - runBlocking { - try { - pushersManager?.unregisterPusher(unifiedPushStore.getEndpointOrToken().orEmpty()) - } catch (e: Exception) { - Timber.d(e, "Probably unregistering a non existing pusher") - } + try { + pushersManager?.unregisterPusher(unifiedPushStore.getEndpointOrToken().orEmpty()) + } catch (e: Exception) { + Timber.d(e, "Probably unregistering a non existing pusher") } unifiedPushStore.storeUpEndpoint(null) unifiedPushStore.storePushGateway(null) From fc66e5f120b7acdd773a14d1888260221f4c1367 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 15:01:17 +0200 Subject: [PATCH 057/314] Ignore if no change is done - bugfix --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index e975f4cf57..53f2045249 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -156,7 +156,7 @@ class UnifiedPushHelper @Inject constructor( .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setItems(distributorsName.toTypedArray()) { _, which -> val distributor = distributors[which] - if (distributor == getCurrentDistributorName()) { + if (distributor == up.getDistributor(context)) { Timber.d("Same distributor selected again, no action") return@setItems } @@ -169,12 +169,10 @@ class UnifiedPushHelper @Inject constructor( up.saveDistributor(context, distributor) Timber.i("Saving distributor: $distributor") up.registerApp(context) + onDoneRunnable?.run() } } .setCancelable(cancellable) - .setOnDismissListener { - onDoneRunnable?.run() - } .show() } From 639c5701501e5c427cec06b016589a29894bab9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 15:23:22 +0200 Subject: [PATCH 058/314] Create extension to get app names --- .../vector/app/core/pushers/UnifiedPushHelper.kt | 16 +++------------- .../vector/app/core/resources/AppNameProvider.kt | 5 ++--- .../java/im/vector/app/core/utils/SystemUtils.kt | 13 +++++++++++++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 53f2045249..88646a778d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -17,7 +17,6 @@ package im.vector.app.core.pushers import android.content.Context -import android.content.pm.PackageManager import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -25,6 +24,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.getApplicationLabel import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences @@ -143,12 +143,7 @@ class UnifiedPushHelper @Inject constructor( if (it == context.packageName) { internalDistributorName } else { - try { - val ai = context.packageManager.getApplicationInfo(it, 0) - context.packageManager.getApplicationLabel(ai) - } catch (e: PackageManager.NameNotFoundException) { - it - } + context.getApplicationLabel(it) } } @@ -248,12 +243,7 @@ class UnifiedPushHelper @Inject constructor( return stringProvider.getString(R.string.unifiedpush_distributor_background_sync) } val distributor = up.getDistributor(context) - return try { - val ai = context.packageManager.getApplicationInfo(distributor, 0) - context.packageManager.getApplicationLabel(ai).toString() - } catch (e: PackageManager.NameNotFoundException) { - distributor - } + return context.getApplicationLabel(distributor) } fun isEmbeddedDistributor(): Boolean { diff --git a/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt b/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt index 90558e35b7..3b6a8b595c 100644 --- a/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt @@ -17,6 +17,7 @@ package im.vector.app.core.resources import android.content.Context +import im.vector.app.core.utils.getApplicationLabel import timber.log.Timber import javax.inject.Inject @@ -25,9 +26,7 @@ class AppNameProvider @Inject constructor(private val context: Context) { fun getAppName(): String { return try { val appPackageName = context.applicationContext.packageName - val pm = context.packageManager - val appInfo = pm.getApplicationInfo(appPackageName, 0) - var appName = pm.getApplicationLabel(appInfo).toString() + var appName = context.getApplicationLabel(appPackageName) // Use appPackageName instead of appName if appName contains any non-ASCII character if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 1939bdf6a9..bb38411980 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -23,6 +23,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.PowerManager @@ -59,6 +60,18 @@ fun Context.isAnimationEnabled(): Boolean { return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f } +/** + * Return the application label of the provided package. If not found, the package is returned. + */ +fun Context.getApplicationLabel(packageName: String): String { + return try { + val ai = packageManager.getApplicationInfo(packageName, 0) + packageManager.getApplicationLabel(ai).toString() + } catch (e: PackageManager.NameNotFoundException) { + packageName + } +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). From 87087197e522ba8215d31e1257bba3db08a5b276 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 15:25:54 +0200 Subject: [PATCH 059/314] shorter code --- .../im/vector/app/core/pushers/UnifiedPushHelper.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 88646a778d..1fd7fcaac5 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -236,14 +236,11 @@ class UnifiedPushHelper @Inject constructor( } fun getCurrentDistributorName(): String { - if (isEmbeddedDistributor()) { - return stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) + return when { + isEmbeddedDistributor() -> stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) + isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) + else -> context.getApplicationLabel(up.getDistributor(context)) } - if (isBackgroundSync()) { - return stringProvider.getString(R.string.unifiedpush_distributor_background_sync) - } - val distributor = up.getDistributor(context) - return context.getApplicationLabel(distributor) } fun isEmbeddedDistributor(): Boolean { From a139756dbc18a866fe05fd947dc21517dc919fed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jun 2022 15:28:27 +0200 Subject: [PATCH 060/314] Fix an issue with empty endpoint. It can happen if the endpoint is manually removed from the distributor. --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 2 +- .../settings/troubleshoot/TestUnifiedPushEndpoint.kt | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 1fd7fcaac5..558c7db911 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -253,7 +253,7 @@ class UnifiedPushHelper @Inject constructor( fun getPrivacyFriendlyUpEndpoint(): String? { val endpoint = unifiedPushStore.getEndpointOrToken() - if (endpoint.isNullOrEmpty()) return endpoint + if (endpoint.isNullOrEmpty()) return null if (isEmbeddedDistributor()) { return endpoint } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt index fefb1d6478..a29d1ad812 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt @@ -30,13 +30,10 @@ class TestUnifiedPushEndpoint @Inject constructor( override fun perform(activityResultLauncher: ActivityResultLauncher) { val endpoint = unifiedPushHelper.getPrivacyFriendlyUpEndpoint() - endpoint?.let { - description = stringProvider.getString( - R.string.settings_troubleshoot_test_current_endpoint_success, - unifiedPushHelper.getPrivacyFriendlyUpEndpoint() - ) + if (endpoint != null) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_success, endpoint) status = TestStatus.SUCCESS - } ?: run { + } else { description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_failed) status = TestStatus.FAILED } From 905934b9d4e9a992bc48cd7ca683b858d26f8706 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jun 2022 12:20:04 +0200 Subject: [PATCH 061/314] Rename method for clarity --- vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt | 2 +- vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt | 2 +- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 6 +++--- .../troubleshoot/TestAvailableUnifiedPushDistributors.kt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7533eae856..388521a96d 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -31,7 +31,7 @@ import im.vector.app.features.settings.VectorPreferences */ object FcmHelper { - fun isPushSupported(): Boolean = false + fun isFirebaseAvailable(): Boolean = false /** * Retrieves the FCM registration token. diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index ef333bb30b..cb569d9c16 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -37,7 +37,7 @@ import timber.log.Timber object FcmHelper { private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" - fun isPushSupported(): Boolean = true + fun isFirebaseAvailable(): Boolean = true /** * Retrieves the FCM registration token. diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 558c7db911..897797b0ab 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -132,7 +132,7 @@ class UnifiedPushHelper @Inject constructor( cancellable: Boolean, ) { val internalDistributorName = stringProvider.getString( - if (FcmHelper.isPushSupported()) { + if (FcmHelper.isFirebaseAvailable()) { R.string.unifiedpush_distributor_fcm_fallback } else { R.string.unifiedpush_distributor_background_sync @@ -244,11 +244,11 @@ class UnifiedPushHelper @Inject constructor( } fun isEmbeddedDistributor(): Boolean { - return up.getDistributor(context) == context.packageName && FcmHelper.isPushSupported() + return up.getDistributor(context) == context.packageName && FcmHelper.isFirebaseAvailable() } fun isBackgroundSync(): Boolean { - return up.getDistributor(context) == context.packageName && !FcmHelper.isPushSupported() + return up.getDistributor(context) == context.packageName && !FcmHelper.isFirebaseAvailable() } fun getPrivacyFriendlyUpEndpoint(): String? { diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt index c51aa22210..a66954b023 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -33,7 +33,7 @@ class TestAvailableUnifiedPushDistributors @Inject constructor( val distributors = unifiedPushHelper.getExternalDistributors() description = if (distributors.isEmpty()) { stringProvider.getString( - if (FcmHelper.isPushSupported()) { + if (FcmHelper.isFirebaseAvailable()) { R.string.settings_troubleshoot_test_distributors_gplay } else { R.string.settings_troubleshoot_test_distributors_fdroid From 3c72ee6e0c5d2e0364723436ed087d8c88b3fe9b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jun 2022 17:02:47 +0200 Subject: [PATCH 062/314] Unregister UP when signing out --- .../main/java/im/vector/app/core/di/ActiveSessionHolder.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index f0c956365f..ef7f0896b8 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -18,6 +18,7 @@ package im.vector.app.core.di import arrow.core.Option import im.vector.app.ActiveSessionDataSource +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler @@ -39,6 +40,7 @@ class ActiveSessionHolder @Inject constructor( private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener, private val imageManager: ImageManager, + private val unifiedPushHelper: UnifiedPushHelper, private val guardServiceStarter: GuardServiceStarter ) { @@ -58,7 +60,7 @@ class ActiveSessionHolder @Inject constructor( guardServiceStarter.start() } - fun clearActiveSession() { + suspend fun clearActiveSession() { // Do some cleanup first getSafeActiveSession()?.let { Timber.w("clearActiveSession of ${it.myUserId}") @@ -72,6 +74,8 @@ class ActiveSessionHolder @Inject constructor( keyRequestHandler.stop() incomingVerificationRequestHandler.stop() pushRuleTriggerListener.stop() + // No need to unregister the pusher, the sign out will (should?) do it server side. + unifiedPushHelper.unregister(pushersManager = null) guardServiceStarter.stop() } From 5846ad57689b68fa3a92cce3f630a19c17ad85a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jun 2022 17:04:26 +0200 Subject: [PATCH 063/314] Inject constructor of `BackgroundSyncStarter` and `FcmHelper` --- .../app/fdroid/BackgroundSyncStarter.kt | 14 +++---- .../OnApplicationUpgradeOrRebootReceiver.kt | 16 ++++---- .../java/im/vector/app/push/fcm/FcmHelper.kt | 24 +++++------- .../troubleshoot/TestFirebaseToken.kt | 5 ++- .../troubleshoot/TestTokenRegistration.kt | 5 ++- .../java/im/vector/app/push/fcm/FcmHelper.kt | 38 +++++++++---------- .../java/im/vector/app/VectorApplication.kt | 7 ++-- .../app/core/pushers/UnifiedPushHelper.kt | 7 ++-- .../vector/app/features/home/HomeActivity.kt | 3 +- .../TestAvailableUnifiedPushDistributors.kt | 3 +- 10 files changed, 59 insertions(+), 63 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt index e2ac4f8822..eaa3d57d42 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -23,14 +23,14 @@ import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import timber.log.Timber +import javax.inject.Inject -object BackgroundSyncStarter { - fun start( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { +class BackgroundSyncStarter @Inject constructor( + private val context: Context, + private val vectorPreferences: VectorPreferences, + private val clock: Clock +) { + fun start(activeSessionHolder: ActiveSessionHolder) { if (vectorPreferences.areNotificationEnabledForDevice()) { val activeSession = activeSessionHolder.getSafeActiveSession() ?: return when (vectorPreferences.getFdroidSyncBackgroundMode()) { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index aacd7723f5..f22aafbeb4 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -20,20 +20,20 @@ package im.vector.app.fdroid.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import im.vector.app.core.extensions.singletonEntryPoint +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.fdroid.BackgroundSyncStarter import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var backgroundSyncStarter: BackgroundSyncStarter + override fun onReceive(context: Context, intent: Intent) { Timber.v("## onReceive() ${intent.action}") - val singletonEntryPoint = context.singletonEntryPoint() - BackgroundSyncStarter.start( - context, - singletonEntryPoint.vectorPreferences(), - singletonEntryPoint.activeSessionHolder(), - singletonEntryPoint.clock() - ) + backgroundSyncStarter.start(activeSessionHolder) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 388521a96d..24ff00a353 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,15 +21,17 @@ import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.time.Clock import im.vector.app.fdroid.BackgroundSyncStarter import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver -import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject /** * This class has an alter ego in the gplay variant. */ -object FcmHelper { +class FcmHelper @Inject constructor( + private val context: Context, + private val backgroundSyncStarter: BackgroundSyncStarter, +) { fun isFirebaseAvailable(): Boolean = false @@ -38,17 +40,16 @@ object FcmHelper { * * @return the FCM token or null if not received from FCM */ - fun getFcmToken(context: Context): String? { + fun getFcmToken(): String? { return null } /** * Store FCM token to the SharedPrefs * - * @param context android context * @param token the token to store */ - fun storeFcmToken(context: Context, token: String?) { + fun storeFcmToken(token: String?) { // No op } @@ -61,18 +62,13 @@ object FcmHelper { // No op } - fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { // try to stop all regardless of background mode activeSessionHolder.getSafeActiveSession()?.syncService()?.stopAnyBackgroundSync() AlarmSyncBroadcastReceiver.cancelAlarm(context) } - fun onEnterBackground( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { - BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder, clock) + fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { + backgroundSyncStarter.start(activeSessionHolder) } } diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 5a9dc90ec4..e7e3157f6b 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -32,7 +32,8 @@ import javax.inject.Inject */ class TestFirebaseToken @Inject constructor( private val context: FragmentActivity, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val fcmHelper: FcmHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { @@ -68,7 +69,7 @@ class TestFirebaseToken @Inject constructor( description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") // Ensure it is well store in our local storage - FcmHelper.storeFcmToken(context, token) + fcmHelper.storeFcmToken(token) } status = TestStatus.SUCCESS } diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index a6220c2018..8c21404d20 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -37,13 +37,14 @@ class TestTokenRegistration @Inject constructor( private val context: FragmentActivity, private val stringProvider: StringProvider, private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder + private val activeSessionHolder: ActiveSessionHolder, + private val fcmHelper: FcmHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Check if we have a registered pusher for this token - val fcmToken = FcmHelper.getFcmToken(context) ?: run { + val fcmToken = fcmHelper.getFcmToken() ?: run { status = TestStatus.FAILED return } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index cb569d9c16..a4eb9efc73 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -26,16 +26,21 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.time.Clock -import im.vector.app.features.settings.VectorPreferences import timber.log.Timber +import javax.inject.Inject /** * This class store the FCM token in SharedPrefs and ensure this token is retrieved. * It has an alter ego in the fdroid variant. */ -object FcmHelper { - private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" +class FcmHelper @Inject constructor( + context: Context, +) { + companion object { + private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + } + + private val sharedPrefs = DefaultSharedPreferences.getInstance(context) fun isFirebaseAvailable(): Boolean = true @@ -44,22 +49,18 @@ object FcmHelper { * * @return the FCM token or null if not received from FCM */ - fun getFcmToken(context: Context): String? { - return DefaultSharedPreferences.getInstance(context).getString(PREFS_KEY_FCM_TOKEN, null) + fun getFcmToken(): String? { + return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) } /** * Store FCM token to the SharedPrefs * TODO Store in realm * - * @param context android context * @param token the token to store */ - fun storeFcmToken( - context: Context, - token: String? - ) { - DefaultSharedPreferences.getInstance(context).edit { + fun storeFcmToken(token: String?) { + sharedPrefs.edit { putString(PREFS_KEY_FCM_TOKEN, token) } } @@ -76,7 +77,7 @@ object FcmHelper { try { FirebaseMessaging.getInstance().token .addOnSuccessListener { token -> - storeFcmToken(activity, token) + storeFcmToken(token) if (registerPusher) { pushersManager.enqueueRegisterPusherWithFcmKey(token) } @@ -98,24 +99,19 @@ object FcmHelper { * it doesn't, display a dialog that allows users to download the APK from * the Google Play Store or enable it in the device's system settings. */ - fun checkPlayServices(context: Context): Boolean { + private fun checkPlayServices(context: Context): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) return resultCode == ConnectionResult.SUCCESS } @Suppress("UNUSED_PARAMETER") - fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { // No op } @Suppress("UNUSED_PARAMETER") - fun onEnterBackground( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { + fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { // No op } } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 7db0f99f5f..e888a257ef 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,7 +43,6 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration @@ -86,7 +85,6 @@ class VectorApplication : @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var clock: Clock @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider @@ -100,6 +98,7 @@ class VectorApplication : @Inject lateinit var vectorFileLogger: VectorFileLogger @Inject lateinit var vectorAnalytics: VectorAnalytics @Inject lateinit var matrix: Matrix + @Inject lateinit var fcmHelper: FcmHelper // font thread handler private var fontThreadHandler: Handler? = null @@ -174,7 +173,7 @@ class VectorApplication : ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { Timber.i("App entered foreground") - FcmHelper.onEnterForeground(appContext, activeSessionHolder) + fcmHelper.onEnterForeground(activeSessionHolder) activeSessionHolder.getSafeActiveSession()?.also { it.syncService().stopAnyBackgroundSync() } @@ -182,7 +181,7 @@ class VectorApplication : override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") - FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder, clock) + fcmHelper.onEnterBackground(activeSessionHolder) } }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 897797b0ab..4c56fd9ad3 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -45,6 +45,7 @@ class UnifiedPushHelper @Inject constructor( private val vectorPreferences: VectorPreferences, private val matrix: Matrix, private val vectorFeatures: VectorFeatures, + private val fcmHelper: FcmHelper, ) { private val up = UnifiedPush @@ -132,7 +133,7 @@ class UnifiedPushHelper @Inject constructor( cancellable: Boolean, ) { val internalDistributorName = stringProvider.getString( - if (FcmHelper.isFirebaseAvailable()) { + if (fcmHelper.isFirebaseAvailable()) { R.string.unifiedpush_distributor_fcm_fallback } else { R.string.unifiedpush_distributor_background_sync @@ -244,11 +245,11 @@ class UnifiedPushHelper @Inject constructor( } fun isEmbeddedDistributor(): Boolean { - return up.getDistributor(context) == context.packageName && FcmHelper.isFirebaseAvailable() + return up.getDistributor(context) == context.packageName && fcmHelper.isFirebaseAvailable() } fun isBackgroundSync(): Boolean { - return up.getDistributor(context) == context.packageName && !FcmHelper.isFirebaseAvailable() + return up.getDistributor(context) == context.packageName && !fcmHelper.isFirebaseAvailable() } fun getPrivacyFriendlyUpEndpoint(): String? { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 26bb72b26f..f2690fa18a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -129,6 +129,7 @@ class HomeActivity : @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var fcmHelper: FcmHelper private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { @@ -191,7 +192,7 @@ class HomeActivity : supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) unifiedPushHelper.register(this) { if (unifiedPushHelper.isEmbeddedDistributor()) { - FcmHelper.ensureFcmTokenIsRetrieved( + fcmHelper.ensureFcmTokenIsRetrieved( this, pushManager, vectorPreferences.areNotificationEnabledForDevice() diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt index a66954b023..acc0142924 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -27,13 +27,14 @@ import javax.inject.Inject class TestAvailableUnifiedPushDistributors @Inject constructor( private val unifiedPushHelper: UnifiedPushHelper, private val stringProvider: StringProvider, + private val fcmHelper: FcmHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { val distributors = unifiedPushHelper.getExternalDistributors() description = if (distributors.isEmpty()) { stringProvider.getString( - if (FcmHelper.isFirebaseAvailable()) { + if (fcmHelper.isFirebaseAvailable()) { R.string.settings_troubleshoot_test_distributors_gplay } else { R.string.settings_troubleshoot_test_distributors_fdroid From 3560ac95d1b55eda07e6b77b337368139134898f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 08:57:33 +0200 Subject: [PATCH 064/314] Create a Kotlin Config object in vector-config module, for easy configuration. --- .../main/java/im/vector/app/config/Config.kt | 37 +++++++++++++++++++ .../im/vector/app/features/VectorFeatures.kt | 12 +----- 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 vector-config/src/main/java/im/vector/app/config/Config.kt diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt new file mode 100644 index 0000000000..414fbcfd8e --- /dev/null +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -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.config + +/** + * Set of flags to configure the application. + */ +object Config { + /** + * Flag to allow external UnifiedPush distributors to be chosen by the user. + * + * Set to true to allow any available external UnifiedPush distributor to be chosen by the user. + * - For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; + * - For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. + * + * Set to false to prevent usage of external UnifiedPush distributors. + * - For Gplay variant it means that only FCM will be used; + * - For F-Droid variant, it means that only background polling will be available to the user. + * + * *Note*: Changing the value from `true` to `false` when the app is already installed on users' phone may have unexpected behavior. + */ + const val ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS = true +} diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 85b04dfbdc..6fe4beff95 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -17,6 +17,7 @@ package im.vector.app.features import im.vector.app.BuildConfig +import im.vector.app.config.Config interface VectorFeatures { @@ -45,15 +46,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false override fun isOnboardingCombinedLoginEnabled() = false - - /** - * Return false to prevent usage of external UnifiedPush distributors. - * - For Gplay variant it means that only FCM will be used; - * - For F-Droid variant, it means that only background polling will be available to the user. - * Return true to allow any available external UnifiedPush distributor to be chosen by the user. - * - For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; - * - For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. - */ - override fun allowExternalUnifiedPushDistributors(): Boolean = true + override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS override fun isScreenSharingEnabled(): Boolean = true } From 2b8d1dd11c3580c00e620c5b78a39403bdd852f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 09:04:37 +0200 Subject: [PATCH 065/314] Write documentation about UnifiedPush. Introduction is inspired from #2743. --- docs/unifiedpush.md | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/unifiedpush.md diff --git a/docs/unifiedpush.md b/docs/unifiedpush.md new file mode 100644 index 0000000000..47eed35fb5 --- /dev/null +++ b/docs/unifiedpush.md @@ -0,0 +1,56 @@ +# UnifiedPush + + + +* [Introduction](#introduction) +* [Configuration in Element-Android and their forks](#configuration-in-element-android-and-their-forks) + * [Enabling and disabling the feature](#enabling-and-disabling-the-feature) + * [Override the configuration at runtime](#override-the-configuration-at-runtime) + * [Enabling the feature](#enabling-the-feature) + * [Disabling the feature](#disabling-the-feature) + * [Useful links](#useful-links) + + + +## Introduction + +The recently started UnifiedPush project is an Android protocol and library for apps to be able to receive distributor-agnostic push notifications. + +The *Gplay* variant of Element Android use the UnifiedPush library to still receive push notifications from FCM, but also alternatively from other non-Google distributor systems that the user can have installed on their device. Currently available are Gotify, a server and app that receives push notifications via a websocket, NoProvider2Push, a peer-to-peer system, and others. This would make it possible to have push notifications without depending on Google services or libraries. + +The UnifiedPush library comes in two variations: the FCM-added version of the library, which basically comes with the FCM distributor built into the library (so a user doesn't need to do anything other than install the app to get FCM notifications), and the main version of the library, which doesn't come with FCM embedded (so a user has to separately install the FCM, Gotify, or other distributor as an app on their phone to get push notifications). + +These two versions of the library are used in the Google Play version and F-Droid version of the app respectively, to be able to publish an easy-to-use no-setup-needed version of the app to Google Play, and a version that doesn't depend on any Google code to F-Droid. + +## Configuration in Element-Android and their forks + +### Enabling and disabling the feature + +Allowing the user to use an alternative distributor can be changed in [Config](../vector-config/src/main/java/im/vector/app/config/Config.kt). The flag is named `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS`. Default value is `true`. + +#### Override the configuration at runtime + +On debug version, it is possible to override this configuration at runtime, using the `Feature` screen. The Feature is named `Allow external UnifiedPush distributors`. + +#### Enabling the feature + +This is the default behavior of Element Android. + +If `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS` is set to true, it allows any available external UnifiedPush distributor to be chosen by the user. +- For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; +- For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. +- On the UI, the setting to choose an alternative distributor will be visible to the user, and some tests in the notification troubleshoot screen will shown. +- For F-Droid, if the user has chosen a distributor, the settings to configure the background polling will be hidden. + +#### Disabling the feature + +If `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS` is set to false, it prevents the usage of external UnifiedPush distributors. +- For Gplay variant it means that only FCM will be used; +- For F-Droid variant, it means that only background polling will be used. +- On the UI, the setting to choose an alternative distributor will be hidden to the user, and some tests in the notification troubleshoot screen will be hidden. + +### Useful links + +- UnifiedPush official website: [https://unifiedpush.org/](https://unifiedpush.org/) +- List of available distributors can be retrieved here: [https://unifiedpush.org/users/distributors/](https://unifiedpush.org/users/distributors/) +- UnifiedPush project discussion can occurs here: [#unifiedpush:matrix.org](https://matrix.to/#/#unifiedpush:matrix.org) From c43122a6f8dedcb6f108dddd5e62378d356ca731 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 09:49:47 +0200 Subject: [PATCH 066/314] Explain why the data are different when received from Firebase and from UnifiedPush. Author: @p1gp1g --- .../core/pushers/VectorMessagingReceiver.kt | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index bb2f253abd..37845337be 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -87,21 +87,12 @@ class VectorMessagingReceiver : MessagingReceiver() { */ override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("## onMessage() received") - val sMessage = String(message) - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessage() %s", sMessage) - } runBlocking { vectorDataStore.incrementPushCounter() } - val moshi = MatrixJsonParser.getMoshi() - val pushData = if (unifiedPushHelper.isEmbeddedDistributor()) { - moshi.adapter(PushDataFcm::class.java).fromJson(sMessage)?.toPushData() - } else { - moshi.adapter(PushDataUnifiedPush::class.java).fromJson(sMessage)?.toPushData() - } ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } + val pushData = parseData(message) ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } // Diagnostic Push if (pushData.eventId == PushersManager.TEST_EVENT_ID) { @@ -125,6 +116,35 @@ class VectorMessagingReceiver : MessagingReceiver() { } } + /** + * Parse the received data from Push. Json format are different depending on the source. + * + * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content + * of the "notification" attribute of the json sent to the gateway [2][3]. + * On the other side, with UnifiedPush, the content of the message received is the content posted to the push + * gateway endpoint [3]. + * + * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. + * + * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py + * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 + * [3] https://spec.matrix.org/latest/push-gateway-api/ + * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) + */ + private fun parseData(message: ByteArray): PushData? { + val sMessage = String(message) + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.tag(loggerTag.value).d("## onMessage() $sMessage") + } + + val moshi = MatrixJsonParser.getMoshi() + return if (unifiedPushHelper.isEmbeddedDistributor()) { + moshi.adapter(PushDataFcm::class.java).fromJson(sMessage)?.toPushData() + } else { + moshi.adapter(PushDataUnifiedPush::class.java).fromJson(sMessage)?.toPushData() + } + } + /** * Called if InstanceID token is updated. This may occur if the security of * the previous token had been compromised. Note that this is also called From 2f2ee1b894e9f4e5904eaad2b17065f562ff5233 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 10:38:38 +0200 Subject: [PATCH 067/314] Format project (only modified files, there are other fomatting issues). --- vector/src/fdroid/AndroidManifest.xml | 5 +++-- vector/src/gplay/AndroidManifest.xml | 4 ++-- vector/src/main/AndroidManifest.xml | 18 +++++++++++------- .../app/core/pushers/UnifiedPushHelper.kt | 4 ++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index ca008043c2..f9adc521c9 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -28,7 +28,8 @@ android:enabled="true" android:exported="false" /> - @@ -48,4 +49,4 @@ - \ No newline at end of file + diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index 5b384d1a0a..c0c0c4ef0f 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -15,8 +15,8 @@ android:exported="false"> - - + + diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 87fd307647..1e7a40cef2 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -304,7 +304,8 @@ android:supportsPictureInPicture="true" /> - @@ -411,13 +412,16 @@ - + - - - - - + + + + + diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 4c56fd9ad3..2e906e2727 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -239,8 +239,8 @@ class UnifiedPushHelper @Inject constructor( fun getCurrentDistributorName(): String { return when { isEmbeddedDistributor() -> stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) - isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) - else -> context.getApplicationLabel(up.getDistributor(context)) + isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) + else -> context.getApplicationLabel(up.getDistributor(context)) } } From 4a546741598730821fc76ea41ffc97f89df736be Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 15:38:31 +0200 Subject: [PATCH 068/314] This module now have Kotlin code. --- vector-config/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector-config/build.gradle b/vector-config/build.gradle index 95b6a6215d..658452bbdd 100644 --- a/vector-config/build.gradle +++ b/vector-config/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'kotlin-android' } android { @@ -13,4 +14,8 @@ android { sourceCompatibility versions.sourceCompat targetCompatibility versions.targetCompat } + + kotlinOptions { + jvmTarget = "11" + } } From 33911c880c5b7b7bad55c2e183422981937fa684 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 15:59:43 +0200 Subject: [PATCH 069/314] Update documentation after @p1gp1g review. --- docs/unifiedpush.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/unifiedpush.md b/docs/unifiedpush.md index 47eed35fb5..2851644e66 100644 --- a/docs/unifiedpush.md +++ b/docs/unifiedpush.md @@ -16,11 +16,13 @@ The recently started UnifiedPush project is an Android protocol and library for apps to be able to receive distributor-agnostic push notifications. -The *Gplay* variant of Element Android use the UnifiedPush library to still receive push notifications from FCM, but also alternatively from other non-Google distributor systems that the user can have installed on their device. Currently available are Gotify, a server and app that receives push notifications via a websocket, NoProvider2Push, a peer-to-peer system, and others. This would make it possible to have push notifications without depending on Google services or libraries. +The *F-Droid* and *Gplay* flavors of Element Android support UnifiedPush, so the user can use any distributor installed on their devices. This would make it possible to have push notifications without depending on Google services or libraries. Currently, the main distributors are [ntfy](https://ntfy.sh) which does not require any setup (like manual registration) to use the public server and [NextPush](https://github.com/UP-NextPush/android), available as a nextcloud application. -The UnifiedPush library comes in two variations: the FCM-added version of the library, which basically comes with the FCM distributor built into the library (so a user doesn't need to do anything other than install the app to get FCM notifications), and the main version of the library, which doesn't come with FCM embedded (so a user has to separately install the FCM, Gotify, or other distributor as an app on their phone to get push notifications). +The *Gplay* variant uses a UnifiedPush library which basically embed a FCM distributor built into the application (so a user doesn't need to do anything other than install the app to get FCM notifications). This variant uses Google Services to receive notifications if the user has not installed any distributor. -These two versions of the library are used in the Google Play version and F-Droid version of the app respectively, to be able to publish an easy-to-use no-setup-needed version of the app to Google Play, and a version that doesn't depend on any Google code to F-Droid. +The *F-Droid* variant does not use this library to avoid any proprietary blob. It will use a polling service if the user has not installed any distributor. + +In all cases, if there are other distributors available, the user will have to opt-in to one of them in the preferences. ## Configuration in Element-Android and their forks From b1e062a204245fdb8c3fe2045d31b3c5088d234f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Jun 2022 17:19:52 +0200 Subject: [PATCH 070/314] Fix small issue on the settings. --- .../VectorSettingsNotificationPreferenceFragment.kt | 6 +++++- vector/src/main/res/xml/vector_settings_notifications.xml | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 47539dd7c3..fa82cc12d3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -101,7 +101,11 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - unifiedPushHelper.register(requireActivity()) + unifiedPushHelper.register(requireActivity()) { + // Update the summary + findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + } } else { unifiedPushHelper.unregister(pushersManager) session.pushersService().refreshPushers() diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index c331f056d1..21590dc0ad 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -52,8 +52,9 @@ android:title="@string/settings_notification_configuration"> From 6681d4fe17f070d2215eafdb0c11508368ab16fa Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 17:39:15 +0100 Subject: [PATCH 071/314] formatting --- .../onboarding/OnboardingViewModel.kt | 42 +++++++++---------- .../onboarding/RegistrationActionHandler.kt | 14 +++---- .../RegistrationWizardActionDelegate.kt | 26 ++++++------ .../onboarding/ftueauth/FtueAuthVariant.kt | 16 +++---- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index fffb1261be..c41c9717f5 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -147,18 +147,18 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() - is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) - is OnboardingAction.ResetAction -> handleResetAction(action) - is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) - OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() - is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName) - OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete() - OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization() - OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile() - is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) - OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() - is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) - OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() + is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) + is OnboardingAction.ResetAction -> handleResetAction(action) + is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() + is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName) + OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete() + OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization() + OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile() + is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) + OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() + is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() } } @@ -276,10 +276,10 @@ class OnboardingViewModel @AssistedInject constructor( runCatching { registrationActionHandler.processAction(awaitState().selectedHomeserver, action) }.fold( onSuccess = { when (it) { - RegistrationActionHandler.Result.Ignored -> { + RegistrationActionHandler.Result.Ignored -> { // do nothing } - is RegistrationActionHandler.Result.NextStage -> { + is RegistrationActionHandler.Result.NextStage -> { overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayRegistrationStage(it.stage)) } is RegistrationActionHandler.Result.RegistrationComplete -> onSessionCreated( @@ -287,11 +287,11 @@ class OnboardingViewModel @AssistedInject constructor( authenticationDescription = awaitState().selectedAuthenticationState.description ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) ) - RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) - RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) - is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) - is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) - RegistrationActionHandler.Result.MissingNextStage -> { + RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) + RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) + is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) + RegistrationActionHandler.Result.MissingNextStage -> { _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found"))) } } @@ -368,8 +368,8 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleUpdateSignMode(action: OnboardingAction.UpdateSignMode) { updateSignMode(action.signMode) when (action.signMode) { - SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration) - SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration) + SignMode.SignIn -> startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.Unknown -> Unit } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index 07a6488676..76b1492cc3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -44,11 +44,11 @@ class RegistrationActionHandler @Inject constructor( val result = registrationWizardActionDelegate.executeAction(action) return when { action.ignoresResult() -> Result.Ignored - else -> when (result) { - is RegistrationResult.Complete -> Result.RegistrationComplete(result.session) - is RegistrationResult.NextStep -> processFlowResult(result, state) + else -> when (result) { + is RegistrationResult.Complete -> Result.RegistrationComplete(result.session) + is RegistrationResult.NextStep -> processFlowResult(result, state) is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email) - is RegistrationResult.Error -> Result.Error(result.cause) + is RegistrationResult.Error -> Result.Error(result.cause) } } } @@ -66,9 +66,9 @@ class RegistrationActionHandler @Inject constructor( private suspend fun handleNextStep(state: SelectedHomeserverState, flowResult: FlowResult): Result { return when { - flowResult.registrationShouldFallback() -> Result.UnsupportedStage + flowResult.registrationShouldFallback() -> Result.UnsupportedStage authenticationService.isRegistrationStarted() -> findNextStage(state, flowResult) - else -> Result.StartRegistration + else -> Result.StartRegistration } } @@ -77,7 +77,7 @@ class RegistrationActionHandler @Inject constructor( state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy( missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator()) ) - else -> flowResult + else -> flowResult } return orderedResult.findNextRegistrationStage() ?.let { Result.NextStage(it) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt index 5ce8bb857b..e27aa9b2ab 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt @@ -36,15 +36,15 @@ class RegistrationWizardActionDelegate @Inject constructor( suspend fun executeAction(action: RegisterAction): RegistrationResult { return when (action) { - RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } - is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } - is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } - is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } - is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) - is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } - is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } + RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } + is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } + is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } + is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } + is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) + is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } + is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } is RegisterAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailIsValidated(registrationWizard, action.delayMillis) - is RegisterAction.CreateAccount -> resultOf { + is RegisterAction.CreateAccount -> resultOf { registrationWizard.createAccount( action.username, action.password, @@ -60,7 +60,7 @@ class RegistrationWizardActionDelegate @Inject constructor( onFailure = { when { action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) - else -> RegistrationResult.Error(it) + else -> RegistrationResult.Error(it) } } ) @@ -72,7 +72,7 @@ class RegistrationWizardActionDelegate @Inject constructor( onFailure = { when { it.is401() -> null // recursively continue to check with a delay - else -> RegistrationResult.Error(it) + else -> RegistrationResult.Error(it) } } ) ?: handleCheckIfEmailIsValidated(registrationWizard, 10_000) @@ -88,7 +88,7 @@ private inline fun resultOf(block: () -> MatrixRegistrationResult): Registration private fun MatrixRegistrationResult.toRegistrationResult() = when (this) { is FlowResponse -> RegistrationResult.NextStep(flowResult) - is Success -> RegistrationResult.Complete(session) + is Success -> RegistrationResult.Complete(session) } sealed interface RegistrationResult { @@ -114,10 +114,10 @@ sealed interface RegisterAction { fun RegisterAction.ignoresResult() = when (this) { is RegisterAction.SendAgainThreePid -> true - else -> false + else -> false } fun RegisterAction.hasLoadingState() = when (this) { is RegisterAction.CheckIfEmailHasBeenValidated -> false - else -> true + else -> true } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 89e28740a4..bae90f1960 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -128,7 +128,7 @@ class FtueAuthVariant( private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) { when (viewEvents) { - is OnboardingViewEvents.OutdatedHomeserver -> { + is OnboardingViewEvents.OutdatedHomeserver -> { MaterialAlertDialogBuilder(activity) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) @@ -223,14 +223,14 @@ class FtueAuthVariant( option = commonOption ) } - OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() - OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() - is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents) - OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog() - is OnboardingViewEvents.DisplayRegistrationStage -> doStage(viewEvents.stage) - OnboardingViewEvents.DisplayStartRegistration -> when { + OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() + OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() + is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents) + OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog() + is OnboardingViewEvents.DisplayRegistrationStage -> doStage(viewEvents.stage) + OnboardingViewEvents.DisplayStartRegistration -> when { vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() - else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) + else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) } } } From 978de65124b16961c57d3a08b415f8d2a4b8726e Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 13 Jun 2022 12:01:21 +0200 Subject: [PATCH 072/314] Fixes large images crashing when opened --- .../features/media/ImageContentRenderer.kt | 73 +------------------ 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 3406e8f7c4..637179cb27 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -18,7 +18,6 @@ package im.vector.app.features.media import android.graphics.Bitmap import android.graphics.drawable.Drawable -import android.net.Uri import android.os.Parcelable import android.view.View import android.widget.ImageView @@ -31,8 +30,6 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.target.Target -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF -import com.github.piasy.biv.view.BigImageView import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.files.LocalFilesHelper @@ -47,8 +44,6 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.api.session.media.PreviewUrlData -import timber.log.Timber -import java.io.File import javax.inject.Inject import kotlin.math.min @@ -142,7 +137,6 @@ class ImageContentRenderer @Inject constructor( else it.dontAnimate() } .transform(cornerTransformation) - // .thumbnail(0.3f) .into(imageView) } @@ -173,47 +167,9 @@ class ImageContentRenderer @Inject constructor( .load(resolvedUrl) } - req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .fitCenter() - .into(target) + req.fitCenter().into(target) } - fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { - val size = processSize(data, mode) - - // a11y - imageView.contentDescription = data.filename - - createGlideRequest(data, mode, imageView, size) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - callback?.invoke(false) - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - callback?.invoke(true) - return false - } - }) - .fitCenter() - .into(imageView) - } - - /** - * onlyRetrieveFromCache is true! - */ fun renderForSharedElementTransition(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { // a11y imageView.contentDescription = data.filename @@ -254,7 +210,6 @@ class ImageContentRenderer @Inject constructor( return false } }) - .onlyRetrieveFromCache(true) .fitCenter() .into(imageView) } @@ -292,32 +247,6 @@ class ImageContentRenderer @Inject constructor( } } - fun render(data: Data, imageView: BigImageView) { - // a11y - imageView.contentDescription = data.filename - - val (width, height) = processSize(data, Mode.THUMBNAIL) - val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val fullSize = resolveUrl(data) - val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) - - if (fullSize.isNullOrBlank() || thumbnail.isNullOrBlank()) { - Timber.w("Invalid urls") - return - } - - imageView.setImageLoaderCallback(object : DefaultImageLoaderCallback { - override fun onSuccess(image: File?) { - imageView.ssiv?.orientation = ORIENTATION_USE_EXIF - } - }) - - imageView.showImage( - Uri.parse(thumbnail), - Uri.parse(fullSize) - ) - } - private fun resolveUrl(data: Data) = (activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) ?: data.url?.takeIf { localFilesHelper.isLocalFile(data.url) && data.allowNonMxcUrls }) From 2f70c1bd4dae88c85ffca391ae818b81c69a4ce0 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 13 Jun 2022 12:10:38 +0200 Subject: [PATCH 073/314] Adds changelog file --- changelog.d/6290.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6290.bugfix diff --git a/changelog.d/6290.bugfix b/changelog.d/6290.bugfix new file mode 100644 index 0000000000..e5ee3e02a6 --- /dev/null +++ b/changelog.d/6290.bugfix @@ -0,0 +1 @@ +Fixed crash when opening large images in the timeline From c7d021ece6a577816a289e1018a9a4127e15a743 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Jun 2022 13:59:09 +0200 Subject: [PATCH 074/314] Extract parser to its own file and add unit test. --- .../im/vector/app/core/pushers/PushParser.kt | 51 +++++++++++ .../core/pushers/VectorMessagingReceiver.kt | 42 ++------- .../vector/app/core/pushers/model/PushData.kt | 2 +- .../app/core/pushers/model/PushDataFcm.kt | 22 +++-- .../core/pushers/model/PushDataUnifiedPush.kt | 26 +++--- .../vector/app/core/pushers/PushParserTest.kt | 85 +++++++++++++++++++ 6 files changed, 173 insertions(+), 55 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/pushers/PushParser.kt create mode 100644 vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt b/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt new file mode 100644 index 0000000000..6f141e3736 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt @@ -0,0 +1,51 @@ +/* + * 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.pushers + +import im.vector.app.core.pushers.model.PushData +import im.vector.app.core.pushers.model.PushDataFcm +import im.vector.app.core.pushers.model.PushDataUnifiedPush +import im.vector.app.core.pushers.model.toPushData +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.MatrixJsonParser +import javax.inject.Inject + +class PushParser @Inject constructor() { + /** + * Parse the received data from Push. Json format are different depending on the source. + * + * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content + * of the "notification" attribute of the json sent to the gateway [2][3]. + * On the other side, with UnifiedPush, the content of the message received is the content posted to the push + * gateway endpoint [3]. + * + * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. + * + * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py + * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 + * [3] https://spec.matrix.org/latest/push-gateway-api/ + * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) + */ + fun parseData(message: String, firebaseFormat: Boolean): PushData? { + val moshi = MatrixJsonParser.getMoshi() + return if (firebaseFormat) { + tryOrNull { moshi.adapter(PushDataFcm::class.java).fromJson(message) }?.toPushData() + } else { + tryOrNull { moshi.adapter(PushDataUnifiedPush::class.java).fromJson(message) }?.toPushData() + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 37845337be..110e5ac386 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -29,9 +29,6 @@ import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.model.PushData -import im.vector.app.core.pushers.model.PushDataFcm -import im.vector.app.core.pushers.model.PushDataUnifiedPush -import im.vector.app.core.pushers.model.toPushData import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager @@ -48,7 +45,6 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.api.util.MatrixJsonParser import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject @@ -70,6 +66,7 @@ class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushStore: UnifiedPushStore + @Inject lateinit var pushParser: PushParser private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -88,11 +85,17 @@ class VectorMessagingReceiver : MessagingReceiver() { override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("## onMessage() received") + val sMessage = String(message) + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.tag(loggerTag.value).d("## onMessage() $sMessage") + } + runBlocking { vectorDataStore.incrementPushCounter() } - val pushData = parseData(message) ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } + val pushData = pushParser.parseData(sMessage, unifiedPushHelper.isEmbeddedDistributor()) + ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } // Diagnostic Push if (pushData.eventId == PushersManager.TEST_EVENT_ID) { @@ -116,35 +119,6 @@ class VectorMessagingReceiver : MessagingReceiver() { } } - /** - * Parse the received data from Push. Json format are different depending on the source. - * - * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content - * of the "notification" attribute of the json sent to the gateway [2][3]. - * On the other side, with UnifiedPush, the content of the message received is the content posted to the push - * gateway endpoint [3]. - * - * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. - * - * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py - * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 - * [3] https://spec.matrix.org/latest/push-gateway-api/ - * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) - */ - private fun parseData(message: ByteArray): PushData? { - val sMessage = String(message) - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessage() $sMessage") - } - - val moshi = MatrixJsonParser.getMoshi() - return if (unifiedPushHelper.isEmbeddedDistributor()) { - moshi.adapter(PushDataFcm::class.java).fromJson(sMessage)?.toPushData() - } else { - moshi.adapter(PushDataUnifiedPush::class.java).fromJson(sMessage)?.toPushData() - } - } - /** * Called if InstanceID token is updated. This may occur if the security of * the previous token had been compromised. Note that this is also called diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt index 9f7b710c91..d6e51ddab2 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt @@ -19,5 +19,5 @@ package im.vector.app.core.pushers.model data class PushData( val eventId: String, val roomId: String, - var unread: Int, + val unread: Int, ) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt index b3bcc69309..8f5c1fb700 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt @@ -26,20 +26,24 @@ import com.squareup.moshi.JsonClass * "event_id":"$anEventId", * "room_id":"!aRoomId", * "unread":"1", - * "prio":"high", + * "prio":"high" * } * * . */ @JsonClass(generateAdapter = true) data class PushDataFcm( - @Json(name = "event_id") val eventId: String = "", - @Json(name = "room_id") val roomId: String = "", - @Json(name = "unread") var unread: Int = 0, + @Json(name = "event_id") val eventId: String, + @Json(name = "room_id") val roomId: String, + @Json(name = "unread") var unread: Int, ) -fun PushDataFcm.toPushData() = PushData( - eventId = eventId, - roomId = roomId, - unread = unread -) +fun PushDataFcm.toPushData(): PushData? { + if (eventId.isEmpty()) return null + if (roomId.isEmpty()) return null + return PushData( + eventId = eventId, + roomId = roomId, + unread = unread + ) +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt index b1410e048f..5883bfc245 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -29,7 +29,7 @@ import com.squareup.moshi.JsonClass * "counts":{ * "unread":1 * }, - * "prio":"high", + * "prio":"high" * } * } * @@ -37,23 +37,27 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class PushDataUnifiedPush( - @Json(name = "notification") val notification: PushDataUnifiedPushNotification = PushDataUnifiedPushNotification() + @Json(name = "notification") val notification: PushDataUnifiedPushNotification ) @JsonClass(generateAdapter = true) data class PushDataUnifiedPushNotification( - @Json(name = "event_id") val eventId: String = "", - @Json(name = "room_id") val roomId: String = "", - @Json(name = "counts") var counts: PushDataUnifiedPushCounts = PushDataUnifiedPushCounts(), + @Json(name = "event_id") val eventId: String, + @Json(name = "room_id") val roomId: String, + @Json(name = "counts") var counts: PushDataUnifiedPushCounts, ) @JsonClass(generateAdapter = true) data class PushDataUnifiedPushCounts( - @Json(name = "unread") val unread: Int = 0 + @Json(name = "unread") val unread: Int ) -fun PushDataUnifiedPush.toPushData() = PushData( - eventId = notification.eventId, - roomId = notification.roomId, - unread = notification.counts.unread -) +fun PushDataUnifiedPush.toPushData(): PushData? { + if (notification.eventId.isEmpty()) return null + if (notification.roomId.isEmpty()) return null + return PushData( + eventId = notification.eventId, + roomId = notification.roomId, + unread = notification.counts.unread + ) +} diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt new file mode 100644 index 0000000000..f247cf55e1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt @@ -0,0 +1,85 @@ +/* + * 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.pushers + +import im.vector.app.core.pushers.model.PushData +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class PushParserTest { + companion object { + private const val UNIFIED_PUSH_DATA = + "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + private const val FIREBASE_PUSH_DATA = + "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"unread\":\"1\",\"prio\":\"high\"}" + } + + private val parsedData = PushData( + eventId = "\$anEventId", + roomId = "!aRoomId", + unread = 1 + ) + + @Test + fun `test edge cases`() { + doAllEdgeTests(true) + doAllEdgeTests(false) + } + + private fun doAllEdgeTests(firebaseFormat: Boolean) { + val pushParser = PushParser() + // Empty string + pushParser.parseData("", firebaseFormat) shouldBe null + // Empty Json + pushParser.parseData("{}", firebaseFormat) shouldBe null + // Bad Json + pushParser.parseData("ABC", firebaseFormat) shouldBe null + } + + @Test + fun `test unified push format`() { + val pushParser = PushParser() + + pushParser.parseData(UNIFIED_PUSH_DATA, false) shouldBeEqualTo parsedData + pushParser.parseData(UNIFIED_PUSH_DATA, true) shouldBe null + } + + @Test + fun `test firebase push format`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA, true) shouldBeEqualTo parsedData + pushParser.parseData(FIREBASE_PUSH_DATA, false) shouldBe null + } + + @Test + fun `test empty roomId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", ""), true) shouldBe null + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", ""), false) shouldBe null + } + + @Test + fun `test empty eventId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", ""), true) shouldBe null + pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", ""), false) shouldBe null + } +} From 2cc881a5d02abdefee760df3b6f3286b84389a92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 12:12:55 +0000 Subject: [PATCH 075/314] Bump dependency-check-gradle from 7.1.0.1 to 7.1.1 Bumps dependency-check-gradle from 7.1.0.1 to 7.1.1. --- updated-dependencies: - dependency-name: org.owasp:dependency-check-gradle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2cb67b7795..3a6022771d 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ buildscript { classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.1.1" - classpath 'org.owasp:dependency-check-gradle:7.1.0.1' + classpath 'org.owasp:dependency-check-gradle:7.1.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" // NOTE: Do not place your application dependencies here; they belong From cffa3270dd33ed14bbfbdc30c842928bb7c96f04 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 13 Jun 2022 15:49:54 +0300 Subject: [PATCH 076/314] Create dummy data for poll view states. --- .../poll/create/CreatePollViewStates.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt new file mode 100644 index 0000000000..ce27176db2 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt @@ -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.poll.create + +import im.vector.app.features.poll.PollMode +import kotlin.random.Random + +object CreatePollViewStates { + + val fakeRoomId = "fakeRoomId" + val fakeEventId = "fakeEventId" + + val createPollArgs = CreatePollArgs(fakeRoomId, null, PollMode.CREATE) + val editPollArgs = CreatePollArgs(fakeRoomId, fakeEventId, PollMode.EDIT) + + val fakeQuestion = "What is your favourite coffee?" + val fakeOptions = List(CreatePollViewModel.MAX_OPTIONS_COUNT + 1) { "Coffee No${Random.nextInt()}" } + + val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy( + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithOnlyQuestion = initialCreatePollViewState.copy( + question = fakeQuestion, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndNotEnoughOptions = initialCreatePollViewState.copy( + question = fakeQuestion, + options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT - 1).toMutableList().apply { add("") }, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithoutQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = "", + options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = fakeQuestion, + options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndMaxOptions = initialCreatePollViewState.copy( + question = fakeQuestion, + options = fakeOptions.take(CreatePollViewModel.MAX_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = false + ) +} From 19de21535bc9b104c453b8e2c89e6471ab6a0ef4 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 13 Jun 2022 15:53:25 +0300 Subject: [PATCH 077/314] Test initial poll view state. --- .../poll/create/CreatePollViewModelTest.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt new file mode 100644 index 0000000000..eceb4a7c13 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -0,0 +1,57 @@ +/* + * 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.poll.create + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.poll.PollMode +import im.vector.app.features.poll.create.CreatePollViewStates.createPollArgs +import im.vector.app.features.poll.create.CreatePollViewStates.editPollArgs +import im.vector.app.features.poll.create.CreatePollViewStates.fakeOptions +import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion +import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState +import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion +import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.test +import org.junit.Rule +import org.junit.Test +import kotlin.random.Random + +class CreatePollViewModelTest { + + @get:Rule + val mvrxTestRule = MvRxTestRule() + + private val fakeSession = FakeSession() + + private fun createPollViewModel(pollMode: PollMode): CreatePollViewModel { + return if (pollMode == PollMode.EDIT) { + CreatePollViewModel(CreatePollViewState(editPollArgs), fakeSession) + } else { + CreatePollViewModel(CreatePollViewState(createPollArgs), fakeSession) + } + } + + @Test + fun `given the view model is initialized then poll cannot be created and options can be added`() { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel + .test() + .assertState(initialCreatePollViewState) + .finish() + } +} From 5b35534c3d5f6f78c0c7fbbb1ce512801005c40b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 13 Jun 2022 15:54:33 +0300 Subject: [PATCH 078/314] Test poll view state without enough options. --- .../poll/create/CreatePollViewModelTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index eceb4a7c13..456911e1e8 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -54,4 +54,29 @@ class CreatePollViewModelTest { .assertState(initialCreatePollViewState) .finish() } + + @Test + fun `given there is not any options when the question is added then poll cannot be created and options can be added`() { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + + createPollViewModel + .test() + .assertState(pollViewStateWithOnlyQuestion) + .finish() + } + + @Test + fun `given there is not enough options when the question is added then poll cannot be created and options can be added`() { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT - 1) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + } + + createPollViewModel + .test() + .assertState(pollViewStateWithQuestionAndNotEnoughOptions) + .finish() + } } From 0bf37abca15dd3cb69936751405135543da8ec9b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 13 Jun 2022 15:58:13 +0300 Subject: [PATCH 079/314] Test poll views state with enough number of options but without a question. --- .../poll/create/CreatePollViewModelTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 456911e1e8..4381035b6f 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -25,6 +25,7 @@ import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions +import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test import org.junit.Rule @@ -79,4 +80,17 @@ class CreatePollViewModelTest { .assertState(pollViewStateWithQuestionAndNotEnoughOptions) .finish() } + + @Test + fun `given there is not a question when enough options are added then poll cannot be created and options can be added`() { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + } + + createPollViewModel + .test() + .assertState(pollViewStateWithoutQuestionAndEnoughOptions) + .finish() + } } From 55bac9ba0f74776546654f4c53dea3f64febab2f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Jun 2022 16:29:45 +0200 Subject: [PATCH 080/314] Give time to the tests to perform --- .../vector/app/ui/robot/settings/SettingsNotificationsRobot.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt index 433a70b5e3..0f54983fcb 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt @@ -42,6 +42,8 @@ class SettingsNotificationsRobot { pressBack() */ clickOnPreference(R.string.settings_notification_troubleshoot) + // Give time for the tests to perform + Thread.sleep(12_000) pressBack() } } From d1e2a903b444e3d33fa01fa355b4d29469e00245 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 13 Jun 2022 16:36:32 +0200 Subject: [PATCH 081/314] Add test for the notification settings. --- .../ui/robot/settings/SettingsNotificationsRobot.kt | 10 ++++++++++ vector/src/main/res/values/strings.xml | 2 +- .../src/main/res/xml/vector_settings_notifications.xml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt index 0f54983fcb..5858e78a2a 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt @@ -17,6 +17,7 @@ package im.vector.app.ui.robot.settings import androidx.test.espresso.Espresso.pressBack +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.espresso.tools.clickOnPreference @@ -41,6 +42,15 @@ class SettingsNotificationsRobot { clickOn(R.string.settings_call_notifications_preferences) pressBack() */ + // Email notification. No Emails are configured so we show go to the screen to add email + clickOnPreference(R.string.settings_notification_emails_no_emails) + assertDisplayed(R.string.settings_emails_and_phone_numbers_title) + pressBack() + + // Display the notification method change dialog + clickOnPreference(R.string.settings_notification_method) + pressBack() + clickOnPreference(R.string.settings_notification_troubleshoot) // Give time for the tests to perform Thread.sleep(12_000) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e8dbdfb644..38a3f3c935 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3073,7 +3073,7 @@ Choose how to receive notifications Google Services Background synchronization - Notification method + Notification method Available methods No other method than Google Play Service found. No other method than background synchronization found. diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 21590dc0ad..6ca2811a72 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -55,7 +55,7 @@ android:dependency="SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" android:key="SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY" android:persistent="false" - android:title="@string/settings_unifiedpush_reregister" /> + android:title="@string/settings_notification_method" /> Date: Mon, 13 Jun 2022 16:38:07 +0200 Subject: [PATCH 082/314] Rename setting key for clarity --- .../java/im/vector/app/features/settings/VectorPreferences.kt | 2 +- .../VectorSettingsNotificationPreferenceFragment.kt | 4 ++-- vector/src/main/res/xml/vector_settings_notifications.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 1b61eb9bcf..276317b557 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -142,7 +142,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" // notification method - const val SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY = "SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY" + const val SETTINGS_NOTIFICATION_METHOD_KEY = "SETTINGS_NOTIFICATION_METHOD_KEY" // Calls const val SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY = "SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index fa82cc12d3..62f5823b65 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -103,7 +103,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( if (isChecked) { unifiedPushHelper.register(requireActivity()) { // Update the summary - findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY) + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) ?.summary = unifiedPushHelper.getCurrentDistributorName() } } else { @@ -151,7 +151,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } - findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.let { if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 6ca2811a72..f4d7ff8cd5 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -53,7 +53,7 @@ From 1f04e73fcb482ff706411f9e89760bbea51fe8b5 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 13 Jun 2022 17:49:57 +0300 Subject: [PATCH 083/314] Test poll view state with a question and enough number of options. --- .../poll/create/CreatePollViewModelTest.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 4381035b6f..ee6fb8db18 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -17,6 +17,7 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.login.SignMode import im.vector.app.features.poll.PollMode import im.vector.app.features.poll.create.CreatePollViewStates.createPollArgs import im.vector.app.features.poll.create.CreatePollViewStates.editPollArgs @@ -24,13 +25,14 @@ import im.vector.app.features.poll.create.CreatePollViewStates.fakeOptions import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion +import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import kotlin.random.Random class CreatePollViewModelTest { @@ -48,7 +50,7 @@ class CreatePollViewModelTest { } @Test - fun `given the view model is initialized then poll cannot be created and options can be added`() { + fun `given the view model is initialized then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) createPollViewModel .test() @@ -57,7 +59,7 @@ class CreatePollViewModelTest { } @Test - fun `given there is not any options when the question is added then poll cannot be created and options can be added`() { + fun `given there is not any options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) @@ -68,7 +70,7 @@ class CreatePollViewModelTest { } @Test - fun `given there is not enough options when the question is added then poll cannot be created and options can be added`() { + fun `given there is not enough options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT - 1) { @@ -82,7 +84,7 @@ class CreatePollViewModelTest { } @Test - fun `given there is not a question when enough options are added then poll cannot be created and options can be added`() { + fun `given there is not a question when enough options are added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) @@ -93,4 +95,18 @@ class CreatePollViewModelTest { .assertState(pollViewStateWithoutQuestionAndEnoughOptions) .finish() } + + @Test + fun `given there is a question when enough options are added then poll can be created and more options can be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + } + + createPollViewModel + .test() + .assertState(pollViewStateWithQuestionAndEnoughOptions) + .finish() + } } From 53c0609c38c7b49e075cdbed31f7dd0b71c168ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 15:37:13 +0000 Subject: [PATCH 084/314] Bump mavericks from 2.6.1 to 2.7.0 Bumps `mavericks` from 2.6.1 to 2.7.0. Updates `mavericks` from 2.6.1 to 2.7.0 - [Release notes](https://github.com/airbnb/mavericks/releases) - [Changelog](https://github.com/airbnb/mavericks/blob/main/CHANGELOG.md) - [Commits](https://github.com/airbnb/mavericks/commits) Updates `mavericks-testing` from 2.6.1 to 2.7.0 - [Release notes](https://github.com/airbnb/mavericks/releases) - [Changelog](https://github.com/airbnb/mavericks/blob/main/CHANGELOG.md) - [Commits](https://github.com/airbnb/mavericks/commits) --- updated-dependencies: - dependency-name: com.airbnb.android:mavericks dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.airbnb.android:mavericks-testing dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 451ad4449b..4463489277 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -22,7 +22,7 @@ def moshi = "1.13.0" def lifecycle = "2.4.1" def flowBinding = "1.2.0" def epoxy = "4.6.2" -def mavericks = "2.6.1" +def mavericks = "2.7.0" def glide = "4.13.2" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" From 9725396582b48bb948db85eeee9f3c6ef1467a95 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 13 Jun 2022 17:41:35 +0100 Subject: [PATCH 085/314] allowing text content types to be shared via android share menu --- changelog.d/6285.feature | 1 + .../im/vector/app/features/attachments/AttachmentsHelper.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6285.feature diff --git a/changelog.d/6285.feature b/changelog.d/6285.feature new file mode 100644 index 0000000000..ce88c13d15 --- /dev/null +++ b/changelog.d/6285.feature @@ -0,0 +1 @@ +Allow sharing text based content via android's share menu (eg .ics files) diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index d5f65a4aef..ffa83b608a 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -214,7 +214,7 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab it.toContentAttachmentData() } ) - } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { + } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("text") || type.startsWith("*")) { callback.onContentAttachmentsReady( MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { it.toContentAttachmentData() From 979dec75dec50ab6efe1da0fafa8d7a9ad75aa3b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 13 Jun 2022 18:06:53 +0100 Subject: [PATCH 086/314] making bugreport screenshots opt in instead of optout --- changelog.d/6261.misc | 1 + vector/src/main/res/layout/activity_bug_report.xml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/6261.misc diff --git a/changelog.d/6261.misc b/changelog.d/6261.misc new file mode 100644 index 0000000000..9ae8bf7b17 --- /dev/null +++ b/changelog.d/6261.misc @@ -0,0 +1 @@ +Making screenshots in bug reports opt in instead of opt out diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml index 8943912ca4..1c019c858d 100644 --- a/vector/src/main/res/layout/activity_bug_report.xml +++ b/vector/src/main/res/layout/activity_bug_report.xml @@ -154,7 +154,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" - android:checked="true" + android:checked="false" android:text="@string/send_bug_report_include_screenshot" /> - \ No newline at end of file + From 7908c4ba2e63e084611b5a6b30cb86f87cd36165 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 23:05:19 +0000 Subject: [PATCH 087/314] Bump actions/setup-python from 3 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/post-pr.yml | 2 +- .github/workflows/sync-from-external-sources.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index 49669e4201..6040fd5f78 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.8 - uses: actions/cache@v3 diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml index 796d915ea6..fdc1bbcb96 100644 --- a/.github/workflows/sync-from-external-sources.yml +++ b/.github/workflows/sync-from-external-sources.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install Prerequisite dependencies @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install Prerequisite dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8d7f2ec33..a8a2d91f70 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: distribution: 'adopt' java-version: '11' - uses: gradle/gradle-build-action@v2 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: 3.8 - uses: michaelkaye/setup-matrix-synapse@v1.0.3 From c10d4a7382946df7eb470263441019635db8df0c Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 14 Jun 2022 10:21:45 +0200 Subject: [PATCH 088/314] Slight formatting improvement --- .../java/im/vector/app/features/media/ImageContentRenderer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 637179cb27..8ce172b829 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -167,7 +167,9 @@ class ImageContentRenderer @Inject constructor( .load(resolvedUrl) } - req.fitCenter().into(target) + req + .fitCenter() + .into(target) } fun renderForSharedElementTransition(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { From c2707d4538c44aa4e9ec3ddbbc727eb01787e8da Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Tue, 14 Jun 2022 14:08:22 +0200 Subject: [PATCH 089/314] Wrong import order --- .../java/im/vector/app/features/call/dialpad/DialPadLookup.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 3ab2ee50c0..8f904c8ab8 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -22,8 +22,8 @@ import im.vector.app.features.call.vectorCallService import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import org.matrix.android.sdk.api.session.Session -import javax.inject.Inject import timber.log.Timber +import javax.inject.Inject class DialPadLookup @Inject constructor( private val session: Session, From 279b9b5d6a377a85334f5065af92f8d8f7dff340 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 16:33:04 +0200 Subject: [PATCH 090/314] Be lenient on the Json format for received data in a Push. --- .../core/pushers/VectorMessagingReceiver.kt | 22 +++++----- .../vector/app/core/pushers/model/PushData.kt | 13 ++++-- .../app/core/pushers/model/PushDataFcm.kt | 21 ++++------ .../core/pushers/model/PushDataUnifiedPush.kt | 25 +++++------ .../vector/app/core/pushers/PushParserTest.kt | 42 ++++++++++++++----- 5 files changed, 73 insertions(+), 50 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 110e5ac386..723d9c2480 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -187,12 +187,12 @@ class VectorMessagingReceiver : MessagingReceiver() { if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { - if (isEventAlreadyKnown(pushData.eventId, pushData.roomId)) { + if (isEventAlreadyKnown(pushData)) { Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, pushData.roomId, pushData.eventId) + getEventFastLane(session, pushData) Timber.tag(loggerTag.value).d("Requesting background sync") session.syncService().requireBackgroundSync() @@ -203,12 +203,12 @@ class VectorMessagingReceiver : MessagingReceiver() { } } - private fun getEventFastLane(session: Session, roomId: String?, eventId: String?) { - roomId?.takeIf { it.isNotEmpty() } ?: return - eventId?.takeIf { it.isNotEmpty() } ?: return + private fun getEventFastLane(session: Session, pushData: PushData) { + pushData.roomId ?: return + pushData.eventId ?: return // If the room is currently displayed, we will not show a notification, so no need to get the Event faster - if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(roomId)) { + if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(pushData.roomId)) { return } @@ -219,7 +219,7 @@ class VectorMessagingReceiver : MessagingReceiver() { coroutineScope.launch { Timber.tag(loggerTag.value).d("Fast lane: start request") - val event = tryOrNull { session.eventService().getEvent(roomId, eventId) } ?: return@launch + val event = tryOrNull { session.eventService().getEvent(pushData.roomId, pushData.eventId) } ?: return@launch val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) @@ -233,12 +233,12 @@ class VectorMessagingReceiver : MessagingReceiver() { // check if the event was not yet received // a previous catchup might have already retrieved the notified event - private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean { - if (null != eventId && null != roomId) { + private fun isEventAlreadyKnown(pushData: PushData): Boolean { + if (pushData.eventId != null && pushData.roomId != null) { try { val session = activeSessionHolder.getSafeActiveSession() ?: return false - val room = session.getRoom(roomId) ?: return false - return room.getTimelineEvent(eventId) != null + val room = session.getRoom(pushData.roomId) ?: return false + return room.getTimelineEvent(pushData.eventId) != null } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") } diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt index d6e51ddab2..d1d095a6fa 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt @@ -16,8 +16,15 @@ package im.vector.app.core.pushers.model +/** + * Represent parsed data that the app has received from a Push content. + * + * @property eventId The Event ID. If not null, it will not be empty, and will have a valid format. + * @property roomId The Room ID. If not null, it will not be empty, and will have a valid format. + * @property unread Number of unread message. + */ data class PushData( - val eventId: String, - val roomId: String, - val unread: Int, + val eventId: String?, + val roomId: String?, + val unread: Int?, ) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt index 8f5c1fb700..1b9c37ae0a 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt @@ -18,6 +18,7 @@ package im.vector.app.core.pushers.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.MatrixPatterns /** * In this case, the format is: @@ -33,17 +34,13 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class PushDataFcm( - @Json(name = "event_id") val eventId: String, - @Json(name = "room_id") val roomId: String, - @Json(name = "unread") var unread: Int, + @Json(name = "event_id") val eventId: String?, + @Json(name = "room_id") val roomId: String?, + @Json(name = "unread") var unread: Int?, ) -fun PushDataFcm.toPushData(): PushData? { - if (eventId.isEmpty()) return null - if (roomId.isEmpty()) return null - return PushData( - eventId = eventId, - roomId = roomId, - unread = unread - ) -} +fun PushDataFcm.toPushData() = PushData( + eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = unread +) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt index 5883bfc245..f4a2f6741d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -18,6 +18,7 @@ package im.vector.app.core.pushers.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.ensureNotEmpty /** * In this case, the format is: @@ -37,27 +38,23 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class PushDataUnifiedPush( - @Json(name = "notification") val notification: PushDataUnifiedPushNotification + @Json(name = "notification") val notification: PushDataUnifiedPushNotification? ) @JsonClass(generateAdapter = true) data class PushDataUnifiedPushNotification( - @Json(name = "event_id") val eventId: String, - @Json(name = "room_id") val roomId: String, - @Json(name = "counts") var counts: PushDataUnifiedPushCounts, + @Json(name = "event_id") val eventId: String?, + @Json(name = "room_id") val roomId: String?, + @Json(name = "counts") var counts: PushDataUnifiedPushCounts?, ) @JsonClass(generateAdapter = true) data class PushDataUnifiedPushCounts( - @Json(name = "unread") val unread: Int + @Json(name = "unread") val unread: Int? ) -fun PushDataUnifiedPush.toPushData(): PushData? { - if (notification.eventId.isEmpty()) return null - if (notification.roomId.isEmpty()) return null - return PushData( - eventId = notification.eventId, - roomId = notification.roomId, - unread = notification.counts.unread - ) -} +fun PushDataUnifiedPush.toPushData() = PushData( + eventId = notification?.eventId?.ensureNotEmpty(), + roomId = notification?.roomId?.ensureNotEmpty(), + unread = notification?.counts?.unread +) diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt index f247cf55e1..62875bb26d 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt @@ -29,12 +29,18 @@ class PushParserTest { "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"unread\":\"1\",\"prio\":\"high\"}" } - private val parsedData = PushData( + private val validData = PushData( eventId = "\$anEventId", roomId = "!aRoomId", unread = 1 ) + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null + ) + @Test fun `test edge cases`() { doAllEdgeTests(true) @@ -46,7 +52,7 @@ class PushParserTest { // Empty string pushParser.parseData("", firebaseFormat) shouldBe null // Empty Json - pushParser.parseData("{}", firebaseFormat) shouldBe null + pushParser.parseData("{}", firebaseFormat) shouldBeEqualTo emptyData // Bad Json pushParser.parseData("ABC", firebaseFormat) shouldBe null } @@ -55,31 +61,47 @@ class PushParserTest { fun `test unified push format`() { val pushParser = PushParser() - pushParser.parseData(UNIFIED_PUSH_DATA, false) shouldBeEqualTo parsedData - pushParser.parseData(UNIFIED_PUSH_DATA, true) shouldBe null + pushParser.parseData(UNIFIED_PUSH_DATA, false) shouldBeEqualTo validData + pushParser.parseData(UNIFIED_PUSH_DATA, true) shouldBeEqualTo emptyData } @Test fun `test firebase push format`() { val pushParser = PushParser() - pushParser.parseData(FIREBASE_PUSH_DATA, true) shouldBeEqualTo parsedData - pushParser.parseData(FIREBASE_PUSH_DATA, false) shouldBe null + pushParser.parseData(FIREBASE_PUSH_DATA, true) shouldBeEqualTo validData + pushParser.parseData(FIREBASE_PUSH_DATA, false) shouldBeEqualTo emptyData } @Test fun `test empty roomId`() { val pushParser = PushParser() - pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", ""), true) shouldBe null - pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", ""), false) shouldBe null + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", ""), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", ""), false) shouldBeEqualTo validData.copy(roomId = null) + } + + @Test + fun `test invalid roomId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", "aRoomId"), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", "aRoomId"), false) shouldBeEqualTo validData.copy(roomId = null) } @Test fun `test empty eventId`() { val pushParser = PushParser() - pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", ""), true) shouldBe null - pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", ""), false) shouldBe null + pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", ""), true) shouldBeEqualTo validData.copy(eventId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", ""), false) shouldBeEqualTo validData.copy(eventId = null) + } + + @Test + fun `test invalid eventId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", "anEventId"), true) shouldBeEqualTo validData.copy(eventId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", "anEventId"), false) shouldBeEqualTo validData.copy(eventId = null) } } From 2174b1105f7db9723335c9d142442f04f062b566 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 16:36:56 +0200 Subject: [PATCH 091/314] Move companion at the bottom of the class. --- .../im/vector/app/core/pushers/UnifiedPushStore.kt | 10 +++++----- .../im/vector/app/core/pushers/PushParserTest.kt | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt index 05e1131c0b..07d291a723 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt @@ -24,11 +24,6 @@ import javax.inject.Inject class UnifiedPushStore @Inject constructor( context: Context, ) { - companion object { - private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" - private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" - } - private val defaultPrefs = DefaultSharedPreferences.getInstance(context) /** @@ -70,4 +65,9 @@ class UnifiedPushStore @Inject constructor( putString(PREFS_PUSH_GATEWAY, gateway) } } + + companion object { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + } } diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt index 62875bb26d..b595203605 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt @@ -22,13 +22,6 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Test class PushParserTest { - companion object { - private const val UNIFIED_PUSH_DATA = - "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" - private const val FIREBASE_PUSH_DATA = - "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"unread\":\"1\",\"prio\":\"high\"}" - } - private val validData = PushData( eventId = "\$anEventId", roomId = "!aRoomId", @@ -104,4 +97,11 @@ class PushParserTest { pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", "anEventId"), true) shouldBeEqualTo validData.copy(eventId = null) pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", "anEventId"), false) shouldBeEqualTo validData.copy(eventId = null) } + + companion object { + private const val UNIFIED_PUSH_DATA = + "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + private const val FIREBASE_PUSH_DATA = + "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"unread\":\"1\",\"prio\":\"high\"}" + } } From 0147eb4b22ee292a1491693fbea9f50a7062a91e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 16:44:21 +0200 Subject: [PATCH 092/314] Fix test --- .../app/core/pushers/model/PushDataUnifiedPush.kt | 6 +++--- .../im/vector/app/core/pushers/PushParserTest.kt | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt index f4a2f6741d..3dbd44f8ae 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -18,7 +18,7 @@ package im.vector.app.core.pushers.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.extensions.ensureNotEmpty +import org.matrix.android.sdk.api.MatrixPatterns /** * In this case, the format is: @@ -54,7 +54,7 @@ data class PushDataUnifiedPushCounts( ) fun PushDataUnifiedPush.toPushData() = PushData( - eventId = notification?.eventId?.ensureNotEmpty(), - roomId = notification?.roomId?.ensureNotEmpty(), + eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, unread = notification?.counts?.unread ) diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt index b595203605..03577a4400 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt @@ -24,7 +24,7 @@ import org.junit.Test class PushParserTest { private val validData = PushData( eventId = "\$anEventId", - roomId = "!aRoomId", + roomId = "!aRoomId:domain", unread = 1 ) @@ -70,16 +70,16 @@ class PushParserTest { fun `test empty roomId`() { val pushParser = PushParser() - pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", ""), true) shouldBeEqualTo validData.copy(roomId = null) - pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", ""), false) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId:domain", ""), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId:domain", ""), false) shouldBeEqualTo validData.copy(roomId = null) } @Test fun `test invalid roomId`() { val pushParser = PushParser() - pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId", "aRoomId"), true) shouldBeEqualTo validData.copy(roomId = null) - pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId", "aRoomId"), false) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId:domain", "aRoomId:domain"), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId:domain", "aRoomId:domain"), false) shouldBeEqualTo validData.copy(roomId = null) } @Test @@ -100,8 +100,8 @@ class PushParserTest { companion object { private const val UNIFIED_PUSH_DATA = - "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId:domain\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" private const val FIREBASE_PUSH_DATA = - "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId\",\"unread\":\"1\",\"prio\":\"high\"}" + "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId:domain\",\"unread\":\"1\",\"prio\":\"high\"}" } } From 49681982ae87ba75ace49772d448c6e37593d094 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 16:55:36 +0200 Subject: [PATCH 093/314] I have done some manual test, this should be fine. --- vector-config/src/main/java/im/vector/app/config/Config.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index 414fbcfd8e..7577e6dba5 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -31,7 +31,9 @@ object Config { * - For Gplay variant it means that only FCM will be used; * - For F-Droid variant, it means that only background polling will be available to the user. * - * *Note*: Changing the value from `true` to `false` when the app is already installed on users' phone may have unexpected behavior. + * *Note*: When the app is already installed on users' phone: + * - Changing the value from `false` to `true` will let the user be able to select an external UnifiedPush distributor; + * - Changing the value from `true` to `false` will force the app to return to the background sync / Firebase Push. */ const val ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS = true } From 6fc278eb2b463388a95e11d86293d6ae118b4249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 27 May 2022 10:55:15 +0200 Subject: [PATCH 094/314] Replace ffmpeg-kit with libopus and libopusenc. --- changelog.d/6203.feature | 1 + dependencies.gradle | 1 + library/opusencoder/.gitignore | 10 + library/opusencoder/build.gradle | 40 + .../opusencoder/src/main/AndroidManifest.xml | 2 + .../opusencoder/src/main/cpp/CMakeLists.txt | 61 ++ .../src/main/cpp/codec/CodecOggOpus.cpp | 43 + .../src/main/cpp/codec/CodecOggOpus.h | 31 + .../src/main/cpp/opus/include/opus.h | 981 ++++++++++++++++++ .../src/main/cpp/opus/include/opus_custom.h | 342 ++++++ .../src/main/cpp/opus/include/opus_defines.h | 788 ++++++++++++++ .../main/cpp/opus/include/opus_multistream.h | 660 ++++++++++++ .../main/cpp/opus/include/opus_projection.h | 568 ++++++++++ .../src/main/cpp/opus/include/opus_types.h | 166 +++ .../src/main/cpp/opus/include/opusenc.h | 358 +++++++ .../main/cpp/opus/libs/arm64-v8a/libopus.so | Bin 0 -> 318136 bytes .../cpp/opus/libs/arm64-v8a/libopusenc.so | Bin 0 -> 49448 bytes .../main/cpp/opus/libs/armeabi-v7a/libopus.so | Bin 0 -> 253712 bytes .../cpp/opus/libs/armeabi-v7a/libopusenc.so | Bin 0 -> 39836 bytes .../src/main/cpp/opus/libs/x86/libopus.so | Bin 0 -> 329156 bytes .../src/main/cpp/opus/libs/x86/libopusenc.so | Bin 0 -> 56796 bytes .../src/main/cpp/opus/libs/x86_64/libopus.so | Bin 0 -> 346688 bytes .../main/cpp/opus/libs/x86_64/libopusenc.so | Bin 0 -> 60864 bytes .../opusencoder/src/main/cpp/opuscodec.cpp | 26 + .../opusencoder/src/main/cpp/utils/Logger.h | 11 + .../im/vector/opusencoder/OggOpusEncoder.kt | 54 + .../opusencoder/configuration/SampleRate.kt | 28 + settings.gradle | 1 + vector/build.gradle | 6 +- .../src/main/assets/open_source_licenses.html | 21 +- .../detail/composer/AudioMessageHelper.kt | 12 +- .../composer/MessageComposerViewModel.kt | 8 +- .../features/voice/AbstractVoiceRecorder.kt | 24 +- .../app/features/voice/VoicePlayerHelper.kt | 73 -- .../app/features/voice/VoiceRecorder.kt | 24 +- .../app/features/voice/VoiceRecorderL.kt | 173 ++- .../features/voice/VoiceRecorderProvider.kt | 5 +- .../app/features/voice/VoiceRecorderQ.kt | 9 +- 38 files changed, 4354 insertions(+), 173 deletions(-) create mode 100644 changelog.d/6203.feature create mode 100644 library/opusencoder/.gitignore create mode 100644 library/opusencoder/build.gradle create mode 100644 library/opusencoder/src/main/AndroidManifest.xml create mode 100644 library/opusencoder/src/main/cpp/CMakeLists.txt create mode 100644 library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp create mode 100644 library/opusencoder/src/main/cpp/codec/CodecOggOpus.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus_custom.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus_defines.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus_multistream.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus_projection.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opus_types.h create mode 100644 library/opusencoder/src/main/cpp/opus/include/opusenc.h create mode 100755 library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopus.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so create mode 100755 library/opusencoder/src/main/cpp/opus/libs/x86_64/libopusenc.so create mode 100644 library/opusencoder/src/main/cpp/opuscodec.cpp create mode 100644 library/opusencoder/src/main/cpp/utils/Logger.h create mode 100644 library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt create mode 100644 library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt delete mode 100644 vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt diff --git a/changelog.d/6203.feature b/changelog.d/6203.feature new file mode 100644 index 0000000000..bbfddd4179 --- /dev/null +++ b/changelog.d/6203.feature @@ -0,0 +1 @@ +Replace ffmpeg-kit with libopus and libopusenc. diff --git a/dependencies.gradle b/dependencies.gradle index 451ad4449b..272a26886b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -48,6 +48,7 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ + 'annotation' : "androidx.annotation:annotation:1.3.0", 'activity' : "androidx.activity:activity:1.4.0", 'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'core' : "androidx.core:core-ktx:1.8.0", diff --git a/library/opusencoder/.gitignore b/library/opusencoder/.gitignore new file mode 100644 index 0000000000..ff535c85f5 --- /dev/null +++ b/library/opusencoder/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +*.cxx +app/.cxx/* +.externalNativeBuild \ No newline at end of file diff --git a/library/opusencoder/build.gradle b/library/opusencoder/build.gradle new file mode 100644 index 0000000000..a825bb98bc --- /dev/null +++ b/library/opusencoder/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + ndkVersion "21.3.6528147" + + compileSdkVersion 31 + + buildToolsVersion "31.0.0" + + defaultConfig { + minSdkVersion 18 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + + externalNativeBuild { + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + } + } +} + +dependencies { + implementation libs.androidx.annotation +} diff --git a/library/opusencoder/src/main/AndroidManifest.xml b/library/opusencoder/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4dd3413820 --- /dev/null +++ b/library/opusencoder/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/library/opusencoder/src/main/cpp/CMakeLists.txt b/library/opusencoder/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..c261af8b90 --- /dev/null +++ b/library/opusencoder/src/main/cpp/CMakeLists.txt @@ -0,0 +1,61 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) +set(CMAKE_CXX_STANDARD 14) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +set(distribution_OPUS_DIR ${CMAKE_SOURCE_DIR}/opus) + +add_library(lib_opus SHARED IMPORTED) +set_target_properties(lib_opus PROPERTIES IMPORTED_LOCATION + ${distribution_OPUS_DIR}/libs/${ANDROID_ABI}/libopus.so) + +add_library(lib_opusenc SHARED IMPORTED) +set_target_properties(lib_opusenc PROPERTIES IMPORTED_LOCATION + ${distribution_OPUS_DIR}/libs/${ANDROID_ABI}/libopusenc.so) + +add_library( # Sets the name of the library. + opuscodec + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + codec/CodecOggOpus.cpp + opuscodec.cpp) + +target_include_directories(opuscodec PRIVATE + ${distribution_OPUS_DIR}/include) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + opuscodec + android + lib_opusenc + lib_opus + # Links the target library to the log library + # included in the NDK. + ${log-lib} ) diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp new file mode 100644 index 0000000000..c5f80ec989 --- /dev/null +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp @@ -0,0 +1,43 @@ +// +// Created by Jorge Martín Espinosa on 30/5/22. +// + +#include "CodecOggOpus.h" +#include "../utils/Logger.h" + +int ret; + +int CodecOggOpus::encoderInit(char* filePath, int sampleRate) { + comments = ope_comments_create(); + int numChannels = 1; // Mono audio + int family = 0; // Channel Mapping Family 0, used for mono/stereo streams + encoder = ope_encoder_create_file(filePath, comments, sampleRate, numChannels, family, &ret); + if (ret != OPE_OK) { + LOGE(TAG, "Creation of OggOpusEnc failed."); + return ret; + } + return OPE_OK; +} + +int CodecOggOpus::setBitrate(int bitrate) { + ret = ope_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST, bitrate); + if (ret != OPE_OK) { + LOGE(TAG, "Could not set bitrate."); + return ret; + } + return OPE_OK; +} + +int CodecOggOpus::writeFrame(short* frame, int samplesPerChannel) { + return ope_encoder_write(encoder, frame, samplesPerChannel); +} + +void CodecOggOpus::encoderRelease() { + ope_encoder_drain(encoder); + ope_encoder_destroy(encoder); + ope_comments_destroy(comments); +} + +CodecOggOpus::~CodecOggOpus() { + encoderRelease(); +} diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h new file mode 100644 index 0000000000..4696a86f64 --- /dev/null +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h @@ -0,0 +1,31 @@ +// +// Created by Jorge Martín Espinosa on 30/5/22. +// + +#ifndef ELEMENT_ANDROID_CODECOGGOPUS_H +#define ELEMENT_ANDROID_CODECOGGOPUS_H + +#include + +class CodecOggOpus { + +private: + const char *TAG = "CodecOggOpus"; + + OggOpusEnc* encoder; + OggOpusComments* comments; + +public: + int encoderInit(char* filePath, int sampleRate); + + int setBitrate(int bitrate); + + int writeFrame(short *frame, int samplesPerChannel); + + void encoderRelease(); + + ~CodecOggOpus(); +}; + + +#endif //ELEMENT_ANDROID_CODECOGGOPUS_H diff --git a/library/opusencoder/src/main/cpp/opus/include/opus.h b/library/opusencoder/src/main/cpp/opus/include/opus.h new file mode 100644 index 0000000000..d282f21d25 --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus.h @@ -0,0 +1,981 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus.h + * @brief Opus reference implementation API + */ + +#ifndef OPUS_H +#define OPUS_H + +#include "opus_types.h" +#include "opus_defines.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @mainpage Opus + * + * The Opus codec is designed for interactive speech and audio transmission over the Internet. + * It is designed by the IETF Codec Working Group and incorporates technology from + * Skype's SILK codec and Xiph.Org's CELT codec. + * + * The Opus codec is designed to handle a wide range of interactive audio applications, + * including Voice over IP, videoconferencing, in-game chat, and even remote live music + * performances. It can scale from low bit-rate narrowband speech to very high quality + * stereo music. Its main features are: + + * @li Sampling rates from 8 to 48 kHz + * @li Bit-rates from 6 kb/s to 510 kb/s + * @li Support for both constant bit-rate (CBR) and variable bit-rate (VBR) + * @li Audio bandwidth from narrowband to full-band + * @li Support for speech and music + * @li Support for mono and stereo + * @li Support for multichannel (up to 255 channels) + * @li Frame sizes from 2.5 ms to 60 ms + * @li Good loss robustness and packet loss concealment (PLC) + * @li Floating point and fixed-point implementation + * + * Documentation sections: + * @li @ref opus_encoder + * @li @ref opus_decoder + * @li @ref opus_repacketizer + * @li @ref opus_multistream + * @li @ref opus_libinfo + * @li @ref opus_custom + */ + +/** @defgroup opus_encoder Opus Encoder + * @{ + * + * @brief This page describes the process and functions used to encode Opus. + * + * Since Opus is a stateful codec, the encoding process starts with creating an encoder + * state. This can be done with: + * + * @code + * int error; + * OpusEncoder *enc; + * enc = opus_encoder_create(Fs, channels, application, &error); + * @endcode + * + * From this point, @c enc can be used for encoding an audio stream. An encoder state + * @b must @b not be used for more than one stream at the same time. Similarly, the encoder + * state @b must @b not be re-initialized for each frame. + * + * While opus_encoder_create() allocates memory for the state, it's also possible + * to initialize pre-allocated memory: + * + * @code + * int size; + * int error; + * OpusEncoder *enc; + * size = opus_encoder_get_size(channels); + * enc = malloc(size); + * error = opus_encoder_init(enc, Fs, channels, application); + * @endcode + * + * where opus_encoder_get_size() returns the required size for the encoder state. Note that + * future versions of this code may change the size, so no assuptions should be made about it. + * + * The encoder state is always continuous in memory and only a shallow copy is sufficient + * to copy it (e.g. memcpy()) + * + * It is possible to change some of the encoder's settings using the opus_encoder_ctl() + * interface. All these settings already default to the recommended value, so they should + * only be changed when necessary. The most common settings one may want to change are: + * + * @code + * opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate)); + * opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity)); + * opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type)); + * @endcode + * + * where + * + * @arg bitrate is in bits per second (b/s) + * @arg complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest + * @arg signal_type is either OPUS_AUTO (default), OPUS_SIGNAL_VOICE, or OPUS_SIGNAL_MUSIC + * + * See @ref opus_encoderctls and @ref opus_genericctls for a complete list of parameters that can be set or queried. Most parameters can be set or changed at any time during a stream. + * + * To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data: + * @code + * len = opus_encode(enc, audio_frame, frame_size, packet, max_packet); + * @endcode + * + * where + *
    + *
  • audio_frame is the audio data in opus_int16 (or float for opus_encode_float())
  • + *
  • frame_size is the duration of the frame in samples (per channel)
  • + *
  • packet is the byte array to which the compressed data is written
  • + *
  • max_packet is the maximum number of bytes that can be written in the packet (4000 bytes is recommended). + * Do not use max_packet to control VBR target bitrate, instead use the #OPUS_SET_BITRATE CTL.
  • + *
+ * + * opus_encode() and opus_encode_float() return the number of bytes actually written to the packet. + * The return value can be negative, which indicates that an error has occurred. If the return value + * is 2 bytes or less, then the packet does not need to be transmitted (DTX). + * + * Once the encoder state if no longer needed, it can be destroyed with + * + * @code + * opus_encoder_destroy(enc); + * @endcode + * + * If the encoder was created with opus_encoder_init() rather than opus_encoder_create(), + * then no action is required aside from potentially freeing the memory that was manually + * allocated for it (calling free(enc) for the example above) + * + */ + +/** Opus encoder state. + * This contains the complete state of an Opus encoder. + * It is position independent and can be freely copied. + * @see opus_encoder_create,opus_encoder_init + */ +typedef struct OpusEncoder OpusEncoder; + +/** Gets the size of an OpusEncoder structure. + * @param[in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); + +/** + */ + +/** Allocates and initializes an encoder state. + * There are three coding modes: + * + * @ref OPUS_APPLICATION_VOIP gives best quality at a given bitrate for voice + * signals. It enhances the input signal by high-pass filtering and + * emphasizing formants and harmonics. Optionally it includes in-band + * forward error correction to protect against packet loss. Use this + * mode for typical VoIP applications. Because of the enhancement, + * even at high bitrates the output may sound different from the input. + * + * @ref OPUS_APPLICATION_AUDIO gives best quality at a given bitrate for most + * non-voice signals like music. Use this mode for music and mixed + * (music/voice) content, broadcast, and applications requiring less + * than 15 ms of coding delay. + * + * @ref OPUS_APPLICATION_RESTRICTED_LOWDELAY configures low-delay mode that + * disables the speech-optimized mode in exchange for slightly reduced delay. + * This mode can only be set on an newly initialized or freshly reset encoder + * because it changes the codec delay. + * + * This is useful when the caller knows that the speech-optimized modes will not be needed (use with caution). + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [out] error int*: @ref opus_errorcodes + * @note Regardless of the sampling rate and number channels selected, the Opus encoder + * can switch to a lower audio bandwidth or number of channels if the bitrate + * selected is too low. This also means that it is safe to always use 48 kHz stereo input + * and let the encoder optimize the encoding. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( + opus_int32 Fs, + int channels, + int application, + int *error +); + +/** Initializes a previously allocated encoder state + * The memory pointed to by st must be at least the size returned by opus_encoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_encoder_create(),opus_encoder_get_size() + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_encoder_init( + OpusEncoder *st, + opus_int32 Fs, + int channels, + int application +) OPUS_ARG_NONNULL(1); + +/** Encodes an Opus frame. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm opus_int16*: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode( + OpusEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes an Opus frame from floating point input. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm float*: Input in float format (interleaved if 2 channels), with a normal range of +/-1.0. + * Samples with a range beyond +/-1.0 are supported but will + * be clipped by decoders using the integer API and should + * only be used if it is known that the far end supports + * extended dynamic range. + * length is frame_size*channels*sizeof(float) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode_float( + OpusEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusEncoder allocated by opus_encoder_create(). + * @param[in] st OpusEncoder*: State to be freed. + */ +OPUS_EXPORT void opus_encoder_destroy(OpusEncoder *st); + +/** Perform a CTL function on an Opus encoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusEncoder*: Encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_encoderctls. + * @see opus_genericctls + * @see opus_encoderctls + */ +OPUS_EXPORT int opus_encoder_ctl(OpusEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); +/**@}*/ + +/** @defgroup opus_decoder Opus Decoder + * @{ + * + * @brief This page describes the process and functions used to decode Opus. + * + * The decoding process also starts with creating a decoder + * state. This can be done with: + * @code + * int error; + * OpusDecoder *dec; + * dec = opus_decoder_create(Fs, channels, &error); + * @endcode + * where + * @li Fs is the sampling rate and must be 8000, 12000, 16000, 24000, or 48000 + * @li channels is the number of channels (1 or 2) + * @li error will hold the error code in case of failure (or #OPUS_OK on success) + * @li the return value is a newly created decoder state to be used for decoding + * + * While opus_decoder_create() allocates memory for the state, it's also possible + * to initialize pre-allocated memory: + * @code + * int size; + * int error; + * OpusDecoder *dec; + * size = opus_decoder_get_size(channels); + * dec = malloc(size); + * error = opus_decoder_init(dec, Fs, channels); + * @endcode + * where opus_decoder_get_size() returns the required size for the decoder state. Note that + * future versions of this code may change the size, so no assuptions should be made about it. + * + * The decoder state is always continuous in memory and only a shallow copy is sufficient + * to copy it (e.g. memcpy()) + * + * To decode a frame, opus_decode() or opus_decode_float() must be called with a packet of compressed audio data: + * @code + * frame_size = opus_decode(dec, packet, len, decoded, max_size, 0); + * @endcode + * where + * + * @li packet is the byte array containing the compressed data + * @li len is the exact number of bytes contained in the packet + * @li decoded is the decoded audio data in opus_int16 (or float for opus_decode_float()) + * @li max_size is the max duration of the frame in samples (per channel) that can fit into the decoded_frame array + * + * opus_decode() and opus_decode_float() return the number of samples (per channel) decoded from the packet. + * If that value is negative, then an error has occurred. This can occur if the packet is corrupted or if the audio + * buffer is too small to hold the decoded audio. + * + * Opus is a stateful codec with overlapping blocks and as a result Opus + * packets are not coded independently of each other. Packets must be + * passed into the decoder serially and in the correct order for a correct + * decode. Lost packets can be replaced with loss concealment by calling + * the decoder with a null pointer and zero length for the missing packet. + * + * A single codec state may only be accessed from a single thread at + * a time and any required locking must be performed by the caller. Separate + * streams must be decoded with separate decoder states and can be decoded + * in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK + * defined. + * + */ + +/** Opus decoder state. + * This contains the complete state of an Opus decoder. + * It is position independent and can be freely copied. + * @see opus_decoder_create,opus_decoder_init + */ +typedef struct OpusDecoder OpusDecoder; + +/** Gets the size of an OpusDecoder structure. + * @param [in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_size(int channels); + +/** Allocates and initializes a decoder state. + * @param [in] Fs opus_int32: Sample rate to decode at (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes + * + * Internally Opus stores data at 48000 Hz, so that should be the default + * value for Fs. However, the decoder can efficiently decode to buffers + * at 8, 12, 16, and 24 kHz so if for some reason the caller cannot use + * data at the full sample rate, or knows the compressed data doesn't + * use the full frequency range, it can request decoding at a reduced + * rate. Likewise, the decoder is capable of filling in either mono or + * interleaved stereo pcm buffers, at the caller's request. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusDecoder *opus_decoder_create( + opus_int32 Fs, + int channels, + int *error +); + +/** Initializes a previously allocated decoder state. + * The state must be at least the size returned by opus_decoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. @see opus_decoder_create,opus_decoder_get_size + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusDecoder*: Decoder state. + * @param [in] Fs opus_int32: Sampling rate to decode to (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_decoder_init( + OpusDecoder *st, + opus_int32 Fs, + int channels +) OPUS_ARG_NONNULL(1); + +/** Decode an Opus packet. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload* + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available, the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode an Opus packet with floating point output. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode_float( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusDecoder*: Decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_decoderctls. + * @see opus_genericctls + * @see opus_decoderctls + */ +OPUS_EXPORT int opus_decoder_ctl(OpusDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusDecoder allocated by opus_decoder_create(). + * @param[in] st OpusDecoder*: State to be freed. + */ +OPUS_EXPORT void opus_decoder_destroy(OpusDecoder *st); + +/** Parse an opus packet into one or more frames. + * Opus_decode will perform this operation internally so most applications do + * not need to use this function. + * This function does not copy the frames, the returned pointers are pointers into + * the input packet. + * @param [in] data char*: Opus packet to be parsed + * @param [in] len opus_int32: size of data + * @param [out] out_toc char*: TOC pointer + * @param [out] frames char*[48] encapsulated frames + * @param [out] size opus_int16[48] sizes of the encapsulated frames + * @param [out] payload_offset int*: returns the position of the payload within the packet (in bytes) + * @returns number of frames + */ +OPUS_EXPORT int opus_packet_parse( + const unsigned char *data, + opus_int32 len, + unsigned char *out_toc, + const unsigned char *frames[48], + opus_int16 size[48], + int *payload_offset +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5); + +/** Gets the bandwidth of an Opus packet. + * @param [in] data char*: Opus packet + * @retval OPUS_BANDWIDTH_NARROWBAND Narrowband (4kHz bandpass) + * @retval OPUS_BANDWIDTH_MEDIUMBAND Mediumband (6kHz bandpass) + * @retval OPUS_BANDWIDTH_WIDEBAND Wideband (8kHz bandpass) + * @retval OPUS_BANDWIDTH_SUPERWIDEBAND Superwideband (12kHz bandpass) + * @retval OPUS_BANDWIDTH_FULLBAND Fullband (20kHz bandpass) + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_bandwidth(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples per frame from an Opus packet. + * @param [in] data char*: Opus packet. + * This must contain at least one byte of + * data. + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples per frame. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_samples_per_frame(const unsigned char *data, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Gets the number of channels from an Opus packet. + * @param [in] data char*: Opus packet + * @returns Number of channels + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_channels(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of frames in an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of frames + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples of an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples of an Opus packet. + * @param [in] dec OpusDecoder*: Decoder state + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_nb_samples(const OpusDecoder *dec, const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + +/** Applies soft-clipping to bring a float signal within the [-1,1] range. If + * the signal is already in that range, nothing is done. If there are values + * outside of [-1,1], then the signal is clipped as smoothly as possible to + * both fit in the range and avoid creating excessive distortion in the + * process. + * @param [in,out] pcm float*: Input PCM and modified PCM + * @param [in] frame_size int Number of samples per channel to process + * @param [in] channels int: Number of channels + * @param [in,out] softclip_mem float*: State memory for the soft clipping process (one float per channel, initialized to zero) + */ +OPUS_EXPORT void opus_pcm_soft_clip(float *pcm, int frame_size, int channels, float *softclip_mem); + + +/**@}*/ + +/** @defgroup opus_repacketizer Repacketizer + * @{ + * + * The repacketizer can be used to merge multiple Opus packets into a single + * packet or alternatively to split Opus packets that have previously been + * merged. Splitting valid Opus packets is always guaranteed to succeed, + * whereas merging valid packets only succeeds if all frames have the same + * mode, bandwidth, and frame size, and when the total duration of the merged + * packet is no more than 120 ms. The 120 ms limit comes from the + * specification and limits decoder memory requirements at a point where + * framing overhead becomes negligible. + * + * The repacketizer currently only operates on elementary Opus + * streams. It will not manipualte multistream packets successfully, except in + * the degenerate case where they consist of data from a single stream. + * + * The repacketizing process starts with creating a repacketizer state, either + * by calling opus_repacketizer_create() or by allocating the memory yourself, + * e.g., + * @code + * OpusRepacketizer *rp; + * rp = (OpusRepacketizer*)malloc(opus_repacketizer_get_size()); + * if (rp != NULL) + * opus_repacketizer_init(rp); + * @endcode + * + * Then the application should submit packets with opus_repacketizer_cat(), + * extract new packets with opus_repacketizer_out() or + * opus_repacketizer_out_range(), and then reset the state for the next set of + * input packets via opus_repacketizer_init(). + * + * For example, to split a sequence of packets into individual frames: + * @code + * unsigned char *data; + * int len; + * while (get_next_packet(&data, &len)) + * { + * unsigned char out[1276]; + * opus_int32 out_len; + * int nb_frames; + * int err; + * int i; + * err = opus_repacketizer_cat(rp, data, len); + * if (err != OPUS_OK) + * { + * release_packet(data); + * return err; + * } + * nb_frames = opus_repacketizer_get_nb_frames(rp); + * for (i = 0; i < nb_frames; i++) + * { + * out_len = opus_repacketizer_out_range(rp, i, i+1, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packet(data); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * } + * opus_repacketizer_init(rp); + * release_packet(data); + * } + * @endcode + * + * Alternatively, to combine a sequence of frames into packets that each + * contain up to TARGET_DURATION_MS milliseconds of data: + * @code + * // The maximum number of packets with duration TARGET_DURATION_MS occurs + * // when the frame size is 2.5 ms, for a total of (TARGET_DURATION_MS*2/5) + * // packets. + * unsigned char *data[(TARGET_DURATION_MS*2/5)+1]; + * opus_int32 len[(TARGET_DURATION_MS*2/5)+1]; + * int nb_packets; + * unsigned char out[1277*(TARGET_DURATION_MS*2/2)]; + * opus_int32 out_len; + * int prev_toc; + * nb_packets = 0; + * while (get_next_packet(data+nb_packets, len+nb_packets)) + * { + * int nb_frames; + * int err; + * nb_frames = opus_packet_get_nb_frames(data[nb_packets], len[nb_packets]); + * if (nb_frames < 1) + * { + * release_packets(data, nb_packets+1); + * return nb_frames; + * } + * nb_frames += opus_repacketizer_get_nb_frames(rp); + * // If adding the next packet would exceed our target, or it has an + * // incompatible TOC sequence, output the packets we already have before + * // submitting it. + * // N.B., The nb_packets > 0 check ensures we've submitted at least one + * // packet since the last call to opus_repacketizer_init(). Otherwise a + * // single packet longer than TARGET_DURATION_MS would cause us to try to + * // output an (invalid) empty packet. It also ensures that prev_toc has + * // been set to a valid value. Additionally, len[nb_packets] > 0 is + * // guaranteed by the call to opus_packet_get_nb_frames() above, so the + * // reference to data[nb_packets][0] should be valid. + * if (nb_packets > 0 && ( + * ((prev_toc & 0xFC) != (data[nb_packets][0] & 0xFC)) || + * opus_packet_get_samples_per_frame(data[nb_packets], 48000)*nb_frames > + * TARGET_DURATION_MS*48)) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packets(data, nb_packets+1); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * opus_repacketizer_init(rp); + * release_packets(data, nb_packets); + * data[0] = data[nb_packets]; + * len[0] = len[nb_packets]; + * nb_packets = 0; + * } + * err = opus_repacketizer_cat(rp, data[nb_packets], len[nb_packets]); + * if (err != OPUS_OK) + * { + * release_packets(data, nb_packets+1); + * return err; + * } + * prev_toc = data[nb_packets][0]; + * nb_packets++; + * } + * // Output the final, partial packet. + * if (nb_packets > 0) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * release_packets(data, nb_packets); + * if (out_len < 0) + * return (int)out_len; + * output_next_packet(out, out_len); + * } + * @endcode + * + * An alternate way of merging packets is to simply call opus_repacketizer_cat() + * unconditionally until it fails. At that point, the merged packet can be + * obtained with opus_repacketizer_out() and the input packet for which + * opus_repacketizer_cat() needs to be re-added to a newly reinitialized + * repacketizer state. + */ + +typedef struct OpusRepacketizer OpusRepacketizer; + +/** Gets the size of an OpusRepacketizer structure. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_size(void); + +/** (Re)initializes a previously allocated repacketizer state. + * The state must be at least the size returned by opus_repacketizer_get_size(). + * This can be used for applications which use their own allocator instead of + * malloc(). + * It must also be called to reset the queue of packets waiting to be + * repacketized, which is necessary if the maximum packet duration of 120 ms + * is reached or if you wish to submit packets with a different Opus + * configuration (coding mode, audio bandwidth, frame size, or channel count). + * Failure to do so will prevent a new packet from being added with + * opus_repacketizer_cat(). + * @see opus_repacketizer_create + * @see opus_repacketizer_get_size + * @see opus_repacketizer_cat + * @param rp OpusRepacketizer*: The repacketizer state to + * (re)initialize. + * @returns A pointer to the same repacketizer state that was passed in. + */ +OPUS_EXPORT OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Allocates memory and initializes the new repacketizer with + * opus_repacketizer_init(). + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusRepacketizer *opus_repacketizer_create(void); + +/** Frees an OpusRepacketizer allocated by + * opus_repacketizer_create(). + * @param[in] rp OpusRepacketizer*: State to be freed. + */ +OPUS_EXPORT void opus_repacketizer_destroy(OpusRepacketizer *rp); + +/** Add a packet to the current repacketizer state. + * This packet must match the configuration of any packets already submitted + * for repacketization since the last call to opus_repacketizer_init(). + * This means that it must have the same coding mode, audio bandwidth, frame + * size, and channel count. + * This can be checked in advance by examining the top 6 bits of the first + * byte of the packet, and ensuring they match the top 6 bits of the first + * byte of any previously submitted packet. + * The total duration of audio in the repacketizer state also must not exceed + * 120 ms, the maximum duration of a single packet, after adding this packet. + * + * The contents of the current repacketizer state can be extracted into new + * packets using opus_repacketizer_out() or opus_repacketizer_out_range(). + * + * In order to add a packet with a different configuration or to add more + * audio beyond 120 ms, you must clear the repacketizer state by calling + * opus_repacketizer_init(). + * If a packet is too large to add to the current repacketizer state, no part + * of it is added, even if it contains multiple frames, some of which might + * fit. + * If you wish to be able to add parts of such packets, you should first use + * another repacketizer to split the packet into pieces and add them + * individually. + * @see opus_repacketizer_out_range + * @see opus_repacketizer_out + * @see opus_repacketizer_init + * @param rp OpusRepacketizer*: The repacketizer state to which to + * add the packet. + * @param[in] data const unsigned char*: The packet data. + * The application must ensure + * this pointer remains valid + * until the next call to + * opus_repacketizer_init() or + * opus_repacketizer_destroy(). + * @param len opus_int32: The number of bytes in the packet data. + * @returns An error code indicating whether or not the operation succeeded. + * @retval #OPUS_OK The packet's contents have been added to the repacketizer + * state. + * @retval #OPUS_INVALID_PACKET The packet did not have a valid TOC sequence, + * the packet's TOC sequence was not compatible + * with previously submitted packets (because + * the coding mode, audio bandwidth, frame size, + * or channel count did not match), or adding + * this packet would increase the total amount of + * audio stored in the repacketizer state to more + * than 120 ms. + */ +OPUS_EXPORT int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param begin int: The index of the first frame in the current + * repacketizer state to include in the output. + * @param end int: One past the index of the last frame in the + * current repacketizer state to include in the + * output. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1276 for a single frame, + * or for multiple frames, + * 1277*(end-begin). + * However, 1*(end-begin) plus + * the size of all packet data submitted to + * the repacketizer since the last call to + * opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG [begin,end) was an invalid range of + * frames (begin < 0, begin >= end, or end > + * opus_repacketizer_get_nb_frames()). + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out_range(OpusRepacketizer *rp, int begin, int end, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Return the total number of frames contained in packet data submitted to + * the repacketizer state so far via opus_repacketizer_cat() since the last + * call to opus_repacketizer_init() or opus_repacketizer_create(). + * This defines the valid range of packets that can be extracted with + * opus_repacketizer_out_range() or opus_repacketizer_out(). + * @param rp OpusRepacketizer*: The repacketizer state containing the + * frames. + * @returns The total number of frames contained in the packet data submitted + * to the repacketizer state. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * This is a convenience routine that returns all the data submitted so far + * in a single packet. + * It is equivalent to calling + * @code + * opus_repacketizer_out_range(rp, 0, opus_repacketizer_get_nb_frames(rp), + * data, maxlen) + * @endcode + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1277*opus_repacketizer_get_nb_frames(rp). + * However, + * 1*opus_repacketizer_get_nb_frames(rp) + * plus the size of all packet data + * submitted to the repacketizer since the + * last call to opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out(OpusRepacketizer *rp, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1); + +/** Pads a given Opus packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len); + +/** Remove all padding from a given Opus packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len); + +/** Pads a given Opus multi-stream packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams); + +/** Remove all padding from a given Opus multi-stream packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_packet_unpad(unsigned char *data, opus_int32 len, int nb_streams); + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_custom.h b/library/opusencoder/src/main/cpp/opus/include/opus_custom.h new file mode 100644 index 0000000000..41f36bf2fb --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus_custom.h @@ -0,0 +1,342 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008-2012 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + @file opus_custom.h + @brief Opus-Custom reference implementation API + */ + +#ifndef OPUS_CUSTOM_H +#define OPUS_CUSTOM_H + +#include "opus_defines.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CUSTOM_MODES +# define OPUS_CUSTOM_EXPORT OPUS_EXPORT +# define OPUS_CUSTOM_EXPORT_STATIC OPUS_EXPORT +#else +# define OPUS_CUSTOM_EXPORT +# ifdef OPUS_BUILD +# define OPUS_CUSTOM_EXPORT_STATIC static OPUS_INLINE +# else +# define OPUS_CUSTOM_EXPORT_STATIC +# endif +#endif + +/** @defgroup opus_custom Opus Custom + * @{ + * Opus Custom is an optional part of the Opus specification and + * reference implementation which uses a distinct API from the regular + * API and supports frame sizes that are not normally supported.\ Use + * of Opus Custom is discouraged for all but very special applications + * for which a frame size different from 2.5, 5, 10, or 20 ms is needed + * (for either complexity or latency reasons) and where interoperability + * is less important. + * + * In addition to the interoperability limitations the use of Opus custom + * disables a substantial chunk of the codec and generally lowers the + * quality available at a given bitrate. Normally when an application needs + * a different frame size from the codec it should buffer to match the + * sizes but this adds a small amount of delay which may be important + * in some very low latency applications. Some transports (especially + * constant rate RF transports) may also work best with frames of + * particular durations. + * + * Libopus only supports custom modes if they are enabled at compile time. + * + * The Opus Custom API is similar to the regular API but the + * @ref opus_encoder_create and @ref opus_decoder_create calls take + * an additional mode parameter which is a structure produced by + * a call to @ref opus_custom_mode_create. Both the encoder and decoder + * must create a mode using the same sample rate (fs) and frame size + * (frame size) so these parameters must either be signaled out of band + * or fixed in a particular implementation. + * + * Similar to regular Opus the custom modes support on the fly frame size + * switching, but the sizes available depend on the particular frame size in + * use. For some initial frame sizes on a single on the fly size is available. + */ + +/** Contains the state of an encoder. One encoder state is needed + for each stream. It is initialized once at the beginning of the + stream. Do *not* re-initialize the state for every frame. + @brief Encoder state + */ +typedef struct OpusCustomEncoder OpusCustomEncoder; + +/** State of the decoder. One decoder state is needed for each stream. + It is initialized once at the beginning of the stream. Do *not* + re-initialize the state for every frame. + @brief Decoder state + */ +typedef struct OpusCustomDecoder OpusCustomDecoder; + +/** The mode contains all the information necessary to create an + encoder. Both the encoder and decoder need to be initialized + with exactly the same mode, otherwise the output will be + corrupted. + @brief Mode configuration + */ +typedef struct OpusCustomMode OpusCustomMode; + +/** Creates a new mode struct. This will be passed to an encoder or + * decoder. The mode MUST NOT BE DESTROYED until the encoders and + * decoders that use it are destroyed as well. + * @param [in] Fs int: Sampling rate (8000 to 96000 Hz) + * @param [in] frame_size int: Number of samples (per channel) to encode in each + * packet (64 - 1024, prime factorization must contain zero or more 2s, 3s, or 5s and no other primes) + * @param [out] error int*: Returned error code (if NULL, no error will be returned) + * @return A newly created mode + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error); + +/** Destroys a mode struct. Only call this after all encoders and + * decoders using this mode are destroyed as well. + * @param [in] mode OpusCustomMode*: Mode to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_mode_destroy(OpusCustomMode *mode); + + +#if !defined(OPUS_BUILD) || defined(CELT_ENCODER_C) + +/* Encoder */ +/** Gets the size of an OpusCustomEncoder structure. + * @param [in] mode OpusCustomMode *: Mode configuration + * @param [in] channels int: Number of channels + * @returns size + */ +OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_encoder_get_size( + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1); + +# ifdef CUSTOM_MODES +/** Initializes a previously allocated encoder state + * The memory pointed to by st must be the size returned by opus_custom_encoder_get_size. + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_custom_encoder_create(),opus_custom_encoder_get_size() + * To reset a previously initialized state use the OPUS_RESET_STATE CTL. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * decoder) + * @param [in] channels int: Number of channels + * @return OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT int opus_custom_encoder_init( + OpusCustomEncoder *st, + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); +# endif +#endif + + +/** Creates a new encoder state. Each stream needs its own encoder + * state (can't be shared across simultaneous streams). + * @param [in] mode OpusCustomMode*: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * decoder) + * @param [in] channels int: Number of channels + * @param [out] error int*: Returns an error code + * @return Newly created encoder state. +*/ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomEncoder *opus_custom_encoder_create( + const OpusCustomMode *mode, + int channels, + int *error +) OPUS_ARG_NONNULL(1); + + +/** Destroys a an encoder state. + * @param[in] st OpusCustomEncoder*: State to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_encoder_destroy(OpusCustomEncoder *st); + +/** Encodes a frame of audio. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] pcm float*: PCM audio in float format, with a normal range of +/-1.0. + * Samples with a range beyond +/-1.0 are supported but will + * be clipped by decoders using the integer API and should + * only be used if it is known that the far end supports + * extended dynamic range. There must be exactly + * frame_size samples per channel. + * @param [in] frame_size int: Number of samples per frame of input signal + * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. + * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame + * (can change from one frame to another) + * @return Number of bytes written to "compressed". + * If negative, an error has occurred (see error codes). It is IMPORTANT that + * the length returned be somehow transmitted to the decoder. Otherwise, no + * decoding is possible. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode_float( + OpusCustomEncoder *st, + const float *pcm, + int frame_size, + unsigned char *compressed, + int maxCompressedBytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a frame of audio. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] pcm opus_int16*: PCM audio in signed 16-bit format (native endian). + * There must be exactly frame_size samples per channel. + * @param [in] frame_size int: Number of samples per frame of input signal + * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. + * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame + * (can change from one frame to another) + * @return Number of bytes written to "compressed". + * If negative, an error has occurred (see error codes). It is IMPORTANT that + * the length returned be somehow transmitted to the decoder. Otherwise, no + * decoding is possible. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode( + OpusCustomEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *compressed, + int maxCompressedBytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus custom encoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @see opus_encoderctls + */ +OPUS_CUSTOM_EXPORT int opus_custom_encoder_ctl(OpusCustomEncoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); + + +#if !defined(OPUS_BUILD) || defined(CELT_DECODER_C) +/* Decoder */ + +/** Gets the size of an OpusCustomDecoder structure. + * @param [in] mode OpusCustomMode *: Mode configuration + * @param [in] channels int: Number of channels + * @returns size + */ +OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_decoder_get_size( + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1); + +/** Initializes a previously allocated decoder state + * The memory pointed to by st must be the size returned by opus_custom_decoder_get_size. + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_custom_decoder_create(),opus_custom_decoder_get_size() + * To reset a previously initialized state use the OPUS_RESET_STATE CTL. + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * encoder) + * @param [in] channels int: Number of channels + * @return OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT_STATIC int opus_custom_decoder_init( + OpusCustomDecoder *st, + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + +#endif + + +/** Creates a new decoder state. Each stream needs its own decoder state (can't + * be shared across simultaneous streams). + * @param [in] mode OpusCustomMode: Contains all the information about the characteristics of the + * stream (must be the same characteristics as used for the encoder) + * @param [in] channels int: Number of channels + * @param [out] error int*: Returns an error code + * @return Newly created decoder state. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomDecoder *opus_custom_decoder_create( + const OpusCustomMode *mode, + int channels, + int *error +) OPUS_ARG_NONNULL(1); + +/** Destroys a an decoder state. + * @param[in] st OpusCustomDecoder*: State to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_decoder_destroy(OpusCustomDecoder *st); + +/** Decode an opus custom frame with floating point output + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len int: Number of bytes in payload + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel of available space in *pcm. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode_float( + OpusCustomDecoder *st, + const unsigned char *data, + int len, + float *pcm, + int frame_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode an opus custom frame + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len int: Number of bytes in payload + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel of available space in *pcm. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode( + OpusCustomDecoder *st, + const unsigned char *data, + int len, + opus_int16 *pcm, + int frame_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus custom decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @see opus_genericctls + */ +OPUS_CUSTOM_EXPORT int opus_custom_decoder_ctl(OpusCustomDecoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_CUSTOM_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_defines.h b/library/opusencoder/src/main/cpp/opus/include/opus_defines.h new file mode 100644 index 0000000000..fbf5d0eb74 --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus_defines.h @@ -0,0 +1,788 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_defines.h + * @brief Opus reference implementation constants + */ + +#ifndef OPUS_DEFINES_H +#define OPUS_DEFINES_H + +#include "opus_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup opus_errorcodes Error codes + * @{ + */ +/** No error @hideinitializer*/ +#define OPUS_OK 0 +/** One or more invalid/out of range arguments @hideinitializer*/ +#define OPUS_BAD_ARG -1 +/** Not enough bytes allocated in the buffer @hideinitializer*/ +#define OPUS_BUFFER_TOO_SMALL -2 +/** An internal error was detected @hideinitializer*/ +#define OPUS_INTERNAL_ERROR -3 +/** The compressed data passed is corrupted @hideinitializer*/ +#define OPUS_INVALID_PACKET -4 +/** Invalid/unsupported request number @hideinitializer*/ +#define OPUS_UNIMPLEMENTED -5 +/** An encoder or decoder structure is invalid or already freed @hideinitializer*/ +#define OPUS_INVALID_STATE -6 +/** Memory allocation has failed @hideinitializer*/ +#define OPUS_ALLOC_FAIL -7 +/**@}*/ + +/** @cond OPUS_INTERNAL_DOC */ +/**Export control for opus functions */ + +#ifndef OPUS_EXPORT +# if defined(WIN32) +# if defined(OPUS_BUILD) && defined(DLL_EXPORT) +# define OPUS_EXPORT __declspec(dllexport) +# else +# define OPUS_EXPORT +# endif +# elif defined(__GNUC__) && defined(OPUS_BUILD) +# define OPUS_EXPORT __attribute__ ((visibility ("default"))) +# else +# define OPUS_EXPORT +# endif +#endif + +# if !defined(OPUS_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define OPUS_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define OPUS_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(3,0) +# define OPUS_RESTRICT __restrict__ +# elif (defined(_MSC_VER) && _MSC_VER >= 1400) +# define OPUS_RESTRICT __restrict +# else +# define OPUS_RESTRICT +# endif +#else +# define OPUS_RESTRICT restrict +#endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(2,7) +# define OPUS_INLINE __inline__ +# elif (defined(_MSC_VER)) +# define OPUS_INLINE __inline +# else +# define OPUS_INLINE +# endif +#else +# define OPUS_INLINE inline +#endif + +/**Warning attributes for opus functions + * NONNULL is not used in OPUS_BUILD to avoid the compiler optimizing out + * some paranoid null checks. */ +#if defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +#else +# define OPUS_WARN_UNUSED_RESULT +#endif +#if !defined(OPUS_BUILD) && defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) +#else +# define OPUS_ARG_NONNULL(_x) +#endif + +/** These are the actual Encoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +#define OPUS_SET_APPLICATION_REQUEST 4000 +#define OPUS_GET_APPLICATION_REQUEST 4001 +#define OPUS_SET_BITRATE_REQUEST 4002 +#define OPUS_GET_BITRATE_REQUEST 4003 +#define OPUS_SET_MAX_BANDWIDTH_REQUEST 4004 +#define OPUS_GET_MAX_BANDWIDTH_REQUEST 4005 +#define OPUS_SET_VBR_REQUEST 4006 +#define OPUS_GET_VBR_REQUEST 4007 +#define OPUS_SET_BANDWIDTH_REQUEST 4008 +#define OPUS_GET_BANDWIDTH_REQUEST 4009 +#define OPUS_SET_COMPLEXITY_REQUEST 4010 +#define OPUS_GET_COMPLEXITY_REQUEST 4011 +#define OPUS_SET_INBAND_FEC_REQUEST 4012 +#define OPUS_GET_INBAND_FEC_REQUEST 4013 +#define OPUS_SET_PACKET_LOSS_PERC_REQUEST 4014 +#define OPUS_GET_PACKET_LOSS_PERC_REQUEST 4015 +#define OPUS_SET_DTX_REQUEST 4016 +#define OPUS_GET_DTX_REQUEST 4017 +#define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020 +#define OPUS_GET_VBR_CONSTRAINT_REQUEST 4021 +#define OPUS_SET_FORCE_CHANNELS_REQUEST 4022 +#define OPUS_GET_FORCE_CHANNELS_REQUEST 4023 +#define OPUS_SET_SIGNAL_REQUEST 4024 +#define OPUS_GET_SIGNAL_REQUEST 4025 +#define OPUS_GET_LOOKAHEAD_REQUEST 4027 +/* #define OPUS_RESET_STATE 4028 */ +#define OPUS_GET_SAMPLE_RATE_REQUEST 4029 +#define OPUS_GET_FINAL_RANGE_REQUEST 4031 +#define OPUS_GET_PITCH_REQUEST 4033 +#define OPUS_SET_GAIN_REQUEST 4034 +#define OPUS_GET_GAIN_REQUEST 4045 /* Should have been 4035 */ +#define OPUS_SET_LSB_DEPTH_REQUEST 4036 +#define OPUS_GET_LSB_DEPTH_REQUEST 4037 +#define OPUS_GET_LAST_PACKET_DURATION_REQUEST 4039 +#define OPUS_SET_EXPERT_FRAME_DURATION_REQUEST 4040 +#define OPUS_GET_EXPERT_FRAME_DURATION_REQUEST 4041 +#define OPUS_SET_PREDICTION_DISABLED_REQUEST 4042 +#define OPUS_GET_PREDICTION_DISABLED_REQUEST 4043 +/* Don't use 4045, it's already taken by OPUS_GET_GAIN_REQUEST */ +#define OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST 4046 +#define OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST 4047 + +/** Defines for the presence of extended APIs. */ +#define OPUS_HAVE_OPUS_PROJECTION_H + +/* Macros to trigger compilation errors when the wrong types are provided to a CTL */ +#define __opus_check_int(x) (((void)((x) == (opus_int32)0)), (opus_int32)(x)) +#define __opus_check_int_ptr(ptr) ((ptr) + ((ptr) - (opus_int32*)(ptr))) +#define __opus_check_uint_ptr(ptr) ((ptr) + ((ptr) - (opus_uint32*)(ptr))) +#define __opus_check_val16_ptr(ptr) ((ptr) + ((ptr) - (opus_val16*)(ptr))) +/** @endcond */ + +/** @defgroup opus_ctlvalues Pre-defined values for CTL interface + * @see opus_genericctls, opus_encoderctls + * @{ + */ +/* Values for the various encoder CTLs */ +#define OPUS_AUTO -1000 /**opus_int32: Allowed values: 0-10, inclusive. + * + * @hideinitializer */ +#define OPUS_SET_COMPLEXITY(x) OPUS_SET_COMPLEXITY_REQUEST, __opus_check_int(x) +/** Gets the encoder's complexity configuration. + * @see OPUS_SET_COMPLEXITY + * @param[out] x opus_int32 *: Returns a value in the range 0-10, + * inclusive. + * @hideinitializer */ +#define OPUS_GET_COMPLEXITY(x) OPUS_GET_COMPLEXITY_REQUEST, __opus_check_int_ptr(x) + +/** Configures the bitrate in the encoder. + * Rates from 500 to 512000 bits per second are meaningful, as well as the + * special values #OPUS_AUTO and #OPUS_BITRATE_MAX. + * The value #OPUS_BITRATE_MAX can be used to cause the codec to use as much + * rate as it can, which is useful for controlling the rate by adjusting the + * output buffer size. + * @see OPUS_GET_BITRATE + * @param[in] x opus_int32: Bitrate in bits per second. The default + * is determined based on the number of + * channels and the input sampling rate. + * @hideinitializer */ +#define OPUS_SET_BITRATE(x) OPUS_SET_BITRATE_REQUEST, __opus_check_int(x) +/** Gets the encoder's bitrate configuration. + * @see OPUS_SET_BITRATE + * @param[out] x opus_int32 *: Returns the bitrate in bits per second. + * The default is determined based on the + * number of channels and the input + * sampling rate. + * @hideinitializer */ +#define OPUS_GET_BITRATE(x) OPUS_GET_BITRATE_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables variable bitrate (VBR) in the encoder. + * The configured bitrate may not be met exactly because frames must + * be an integer number of bytes in length. + * @see OPUS_GET_VBR + * @see OPUS_SET_VBR_CONSTRAINT + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Hard CBR. For LPC/hybrid modes at very low bit-rate, this can + * cause noticeable quality degradation.
+ *
1
VBR (default). The exact type of VBR is controlled by + * #OPUS_SET_VBR_CONSTRAINT.
+ *
+ * @hideinitializer */ +#define OPUS_SET_VBR(x) OPUS_SET_VBR_REQUEST, __opus_check_int(x) +/** Determine if variable bitrate (VBR) is enabled in the encoder. + * @see OPUS_SET_VBR + * @see OPUS_GET_VBR_CONSTRAINT + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Hard CBR.
+ *
1
VBR (default). The exact type of VBR may be retrieved via + * #OPUS_GET_VBR_CONSTRAINT.
+ *
+ * @hideinitializer */ +#define OPUS_GET_VBR(x) OPUS_GET_VBR_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables constrained VBR in the encoder. + * This setting is ignored when the encoder is in CBR mode. + * @warning Only the MDCT mode of Opus currently heeds the constraint. + * Speech mode ignores it completely, hybrid mode may fail to obey it + * if the LPC layer uses more bitrate than the constraint would have + * permitted. + * @see OPUS_GET_VBR_CONSTRAINT + * @see OPUS_SET_VBR + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Unconstrained VBR.
+ *
1
Constrained VBR (default). This creates a maximum of one + * frame of buffering delay assuming a transport with a + * serialization speed of the nominal bitrate.
+ *
+ * @hideinitializer */ +#define OPUS_SET_VBR_CONSTRAINT(x) OPUS_SET_VBR_CONSTRAINT_REQUEST, __opus_check_int(x) +/** Determine if constrained VBR is enabled in the encoder. + * @see OPUS_SET_VBR_CONSTRAINT + * @see OPUS_GET_VBR + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Unconstrained VBR.
+ *
1
Constrained VBR (default).
+ *
+ * @hideinitializer */ +#define OPUS_GET_VBR_CONSTRAINT(x) OPUS_GET_VBR_CONSTRAINT_REQUEST, __opus_check_int_ptr(x) + +/** Configures mono/stereo forcing in the encoder. + * This can force the encoder to produce packets encoded as either mono or + * stereo, regardless of the format of the input audio. This is useful when + * the caller knows that the input signal is currently a mono source embedded + * in a stereo stream. + * @see OPUS_GET_FORCE_CHANNELS + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
Not forced (default)
+ *
1
Forced mono
+ *
2
Forced stereo
+ *
+ * @hideinitializer */ +#define OPUS_SET_FORCE_CHANNELS(x) OPUS_SET_FORCE_CHANNELS_REQUEST, __opus_check_int(x) +/** Gets the encoder's forced channel configuration. + * @see OPUS_SET_FORCE_CHANNELS + * @param[out] x opus_int32 *: + *
+ *
#OPUS_AUTO
Not forced (default)
+ *
1
Forced mono
+ *
2
Forced stereo
+ *
+ * @hideinitializer */ +#define OPUS_GET_FORCE_CHANNELS(x) OPUS_GET_FORCE_CHANNELS_REQUEST, __opus_check_int_ptr(x) + +/** Configures the maximum bandpass that the encoder will select automatically. + * Applications should normally use this instead of #OPUS_SET_BANDWIDTH + * (leaving that set to the default, #OPUS_AUTO). This allows the + * application to set an upper bound based on the type of input it is + * providing, but still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_MAX_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
+ *
OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
+ *
+ * @hideinitializer */ +#define OPUS_SET_MAX_BANDWIDTH(x) OPUS_SET_MAX_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Gets the encoder's configured maximum allowed bandpass. + * @see OPUS_SET_MAX_BANDWIDTH + * @param[out] x opus_int32 *: Allowed values: + *
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
+ *
+ * @hideinitializer */ +#define OPUS_GET_MAX_BANDWIDTH(x) OPUS_GET_MAX_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Sets the encoder's bandpass to a specific value. + * This prevents the encoder from automatically selecting the bandpass based + * on the available bitrate. If an application knows the bandpass of the input + * audio it is providing, it should normally use #OPUS_SET_MAX_BANDWIDTH + * instead, which still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
+ *
+ * @hideinitializer */ +#define OPUS_SET_BANDWIDTH(x) OPUS_SET_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Configures the type of signal being encoded. + * This is a hint which helps the encoder's mode selection. + * @see OPUS_GET_SIGNAL + * @param[in] x opus_int32: Allowed values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
+ *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
+ *
+ * @hideinitializer */ +#define OPUS_SET_SIGNAL(x) OPUS_SET_SIGNAL_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal type. + * @see OPUS_SET_SIGNAL + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
+ *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
+ *
+ * @hideinitializer */ +#define OPUS_GET_SIGNAL(x) OPUS_GET_SIGNAL_REQUEST, __opus_check_int_ptr(x) + + +/** Configures the encoder's intended application. + * The initial value is a mandatory argument to the encoder_create function. + * @see OPUS_GET_APPLICATION + * @param[in] x opus_int32: Returns one of the following values: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @hideinitializer */ +#define OPUS_SET_APPLICATION(x) OPUS_SET_APPLICATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured application. + * @see OPUS_SET_APPLICATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @hideinitializer */ +#define OPUS_GET_APPLICATION(x) OPUS_GET_APPLICATION_REQUEST, __opus_check_int_ptr(x) + +/** Gets the total samples of delay added by the entire codec. + * This can be queried by the encoder and then the provided number of samples can be + * skipped on from the start of the decoder's output to provide time aligned input + * and output. From the perspective of a decoding application the real data begins this many + * samples late. + * + * The decoder contribution to this delay is identical for all decoders, but the + * encoder portion of the delay may vary from implementation to implementation, + * version to version, or even depend on the encoder's initial configuration. + * Applications needing delay compensation should call this CTL rather than + * hard-coding a value. + * @param[out] x opus_int32 *: Number of lookahead samples + * @hideinitializer */ +#define OPUS_GET_LOOKAHEAD(x) OPUS_GET_LOOKAHEAD_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of inband forward error correction (FEC). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_INBAND_FEC + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Disable inband FEC (default).
+ *
1
Enable inband FEC.
+ *
+ * @hideinitializer */ +#define OPUS_SET_INBAND_FEC(x) OPUS_SET_INBAND_FEC_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of inband forward error correction. + * @see OPUS_SET_INBAND_FEC + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Inband FEC disabled (default).
+ *
1
Inband FEC enabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_INBAND_FEC(x) OPUS_GET_INBAND_FEC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's expected packet loss percentage. + * Higher values trigger progressively more loss resistant behavior in the encoder + * at the expense of quality at a given bitrate in the absence of packet loss, but + * greater quality under loss. + * @see OPUS_GET_PACKET_LOSS_PERC + * @param[in] x opus_int32: Loss percentage in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_SET_PACKET_LOSS_PERC(x) OPUS_SET_PACKET_LOSS_PERC_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured packet loss percentage. + * @see OPUS_SET_PACKET_LOSS_PERC + * @param[out] x opus_int32 *: Returns the configured loss percentage + * in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_GET_PACKET_LOSS_PERC(x) OPUS_GET_PACKET_LOSS_PERC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of discontinuous transmission (DTX). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_DTX + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Disable DTX (default).
+ *
1
Enabled DTX.
+ *
+ * @hideinitializer */ +#define OPUS_SET_DTX(x) OPUS_SET_DTX_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of discontinuous transmission. + * @see OPUS_SET_DTX + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
DTX disabled (default).
+ *
1
DTX enabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_DTX(x) OPUS_GET_DTX_REQUEST, __opus_check_int_ptr(x) +/** Configures the depth of signal being encoded. + * + * This is a hint which helps the encoder identify silence and near-silence. + * It represents the number of significant bits of linear intensity below + * which the signal contains ignorable quantization or other noise. + * + * For example, OPUS_SET_LSB_DEPTH(14) would be an appropriate setting + * for G.711 u-law input. OPUS_SET_LSB_DEPTH(16) would be appropriate + * for 16-bit linear pcm input with opus_encode_float(). + * + * When using opus_encode() instead of opus_encode_float(), or when libopus + * is compiled for fixed-point, the encoder uses the minimum of the value + * set here and the value 16. + * + * @see OPUS_GET_LSB_DEPTH + * @param[in] x opus_int32: Input precision in bits, between 8 and 24 + * (default: 24). + * @hideinitializer */ +#define OPUS_SET_LSB_DEPTH(x) OPUS_SET_LSB_DEPTH_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal depth. + * @see OPUS_SET_LSB_DEPTH + * @param[out] x opus_int32 *: Input precision in bits, between 8 and + * 24 (default: 24). + * @hideinitializer */ +#define OPUS_GET_LSB_DEPTH(x) OPUS_GET_LSB_DEPTH_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of variable duration frames. + * When variable duration is enabled, the encoder is free to use a shorter frame + * size than the one requested in the opus_encode*() call. + * It is then the user's responsibility + * to verify how much audio was encoded by checking the ToC byte of the encoded + * packet. The part of the audio that was not encoded needs to be resent to the + * encoder for the next call. Do not use this option unless you really + * know what you are doing. + * @see OPUS_GET_EXPERT_FRAME_DURATION + * @param[in] x opus_int32: Allowed values: + *
+ *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
+ *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
+ *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
+ *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
+ *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
+ *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
+ *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
+ *
OPUS_FRAMESIZE_80_MS
Use 80 ms frames.
+ *
OPUS_FRAMESIZE_100_MS
Use 100 ms frames.
+ *
OPUS_FRAMESIZE_120_MS
Use 120 ms frames.
+ *
+ * @hideinitializer */ +#define OPUS_SET_EXPERT_FRAME_DURATION(x) OPUS_SET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured use of variable duration frames. + * @see OPUS_SET_EXPERT_FRAME_DURATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
+ *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
+ *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
+ *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
+ *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
+ *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
+ *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
+ *
OPUS_FRAMESIZE_80_MS
Use 80 ms frames.
+ *
OPUS_FRAMESIZE_100_MS
Use 100 ms frames.
+ *
OPUS_FRAMESIZE_120_MS
Use 120 ms frames.
+ *
+ * @hideinitializer */ +#define OPUS_GET_EXPERT_FRAME_DURATION(x) OPUS_GET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** If set to 1, disables almost all use of prediction, making frames almost + * completely independent. This reduces quality. + * @see OPUS_GET_PREDICTION_DISABLED + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Enable prediction (default).
+ *
1
Disable prediction.
+ *
+ * @hideinitializer */ +#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured prediction status. + * @see OPUS_SET_PREDICTION_DISABLED + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Prediction enabled (default).
+ *
1
Prediction disabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_PREDICTION_DISABLED(x) OPUS_GET_PREDICTION_DISABLED_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_genericctls Generic CTLs + * + * These macros are used with the \c opus_decoder_ctl and + * \c opus_encoder_ctl calls to generate a particular + * request. + * + * When called on an \c OpusDecoder they apply to that + * particular decoder instance. When called on an + * \c OpusEncoder they apply to the corresponding setting + * on that encoder instance, if present. + * + * Some usage examples: + * + * @code + * int ret; + * opus_int32 pitch; + * ret = opus_decoder_ctl(dec_ctx, OPUS_GET_PITCH(&pitch)); + * if (ret == OPUS_OK) return ret; + * + * opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE); + * opus_decoder_ctl(dec_ctx, OPUS_RESET_STATE); + * + * opus_int32 enc_bw, dec_bw; + * opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&enc_bw)); + * opus_decoder_ctl(dec_ctx, OPUS_GET_BANDWIDTH(&dec_bw)); + * if (enc_bw != dec_bw) { + * printf("packet bandwidth mismatch!\n"); + * } + * @endcode + * + * @see opus_encoder, opus_decoder_ctl, opus_encoder_ctl, opus_decoderctls, opus_encoderctls + * @{ + */ + +/** Resets the codec state to be equivalent to a freshly initialized state. + * This should be called when switching streams in order to prevent + * the back to back decoding from giving different results from + * one at a time decoding. + * @hideinitializer */ +#define OPUS_RESET_STATE 4028 + +/** Gets the final state of the codec's entropy coder. + * This is used for testing purposes, + * The encoder and decoder state should be identical after coding a payload + * (assuming no data corruption or software bugs) + * + * @param[out] x opus_uint32 *: Entropy coder state + * + * @hideinitializer */ +#define OPUS_GET_FINAL_RANGE(x) OPUS_GET_FINAL_RANGE_REQUEST, __opus_check_uint_ptr(x) + +/** Gets the encoder's configured bandpass or the decoder's last bandpass. + * @see OPUS_SET_BANDWIDTH + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
#OPUS_AUTO
(default)
+ *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
+ *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
+ *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
+ *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
+ *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
+ *
+ * @hideinitializer */ +#define OPUS_GET_BANDWIDTH(x) OPUS_GET_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Gets the sampling rate the encoder or decoder was initialized with. + * This simply returns the Fs value passed to opus_encoder_init() + * or opus_decoder_init(). + * @param[out] x opus_int32 *: Sampling rate of encoder or decoder. + * @hideinitializer + */ +#define OPUS_GET_SAMPLE_RATE(x) OPUS_GET_SAMPLE_RATE_REQUEST, __opus_check_int_ptr(x) + +/** If set to 1, disables the use of phase inversion for intensity stereo, + * improving the quality of mono downmixes, but slightly reducing normal + * stereo quality. Disabling phase inversion in the decoder does not comply + * with RFC 6716, although it does not cause any interoperability issue and + * is expected to become part of the Opus standard once RFC 6716 is updated + * by draft-ietf-codec-opus-update. + * @see OPUS_GET_PHASE_INVERSION_DISABLED + * @param[in] x opus_int32: Allowed values: + *
+ *
0
Enable phase inversion (default).
+ *
1
Disable phase inversion.
+ *
+ * @hideinitializer */ +#define OPUS_SET_PHASE_INVERSION_DISABLED(x) OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured phase inversion status. + * @see OPUS_SET_PHASE_INVERSION_DISABLED + * @param[out] x opus_int32 *: Returns one of the following values: + *
+ *
0
Stereo phase inversion enabled (default).
+ *
1
Stereo phase inversion disabled.
+ *
+ * @hideinitializer */ +#define OPUS_GET_PHASE_INVERSION_DISABLED(x) OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_decoderctls Decoder related CTLs + * @see opus_genericctls, opus_encoderctls, opus_decoder + * @{ + */ + +/** Configures decoder gain adjustment. + * Scales the decoded output by a factor specified in Q8 dB units. + * This has a maximum range of -32768 to 32767 inclusive, and returns + * OPUS_BAD_ARG otherwise. The default is zero indicating no adjustment. + * This setting survives decoder reset. + * + * gain = pow(10, x/(20.0*256)) + * + * @param[in] x opus_int32: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_SET_GAIN(x) OPUS_SET_GAIN_REQUEST, __opus_check_int(x) +/** Gets the decoder's configured gain adjustment. @see OPUS_SET_GAIN + * + * @param[out] x opus_int32 *: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_GET_GAIN(x) OPUS_GET_GAIN_REQUEST, __opus_check_int_ptr(x) + +/** Gets the duration (in samples) of the last packet successfully decoded or concealed. + * @param[out] x opus_int32 *: Number of samples (at current sampling rate). + * @hideinitializer */ +#define OPUS_GET_LAST_PACKET_DURATION(x) OPUS_GET_LAST_PACKET_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** Gets the pitch of the last decoded frame, if available. + * This can be used for any post-processing algorithm requiring the use of pitch, + * e.g. time stretching/shortening. If the last frame was not voiced, or if the + * pitch was not coded in the frame, then zero is returned. + * + * This CTL is only implemented for decoder instances. + * + * @param[out] x opus_int32 *: pitch period at 48 kHz (or 0 if not available) + * + * @hideinitializer */ +#define OPUS_GET_PITCH(x) OPUS_GET_PITCH_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_libinfo Opus library information functions + * @{ + */ + +/** Converts an opus error code into a human readable string. + * + * @param[in] error int: Error number + * @returns Error string + */ +OPUS_EXPORT const char *opus_strerror(int error); + +/** Gets the libopus version string. + * + * Applications may look for the substring "-fixed" in the version string to + * determine whether they have a fixed-point or floating-point build at + * runtime. + * + * @returns Version string + */ +OPUS_EXPORT const char *opus_get_version_string(void); +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_DEFINES_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h b/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h new file mode 100644 index 0000000000..babcee6905 --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h @@ -0,0 +1,660 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_multistream.h + * @brief Opus reference implementation multistream API + */ + +#ifndef OPUS_MULTISTREAM_H +#define OPUS_MULTISTREAM_H + +#include "opus.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond OPUS_INTERNAL_DOC */ + +/** Macros to trigger compilation errors when the wrong types are provided to a + * CTL. */ +/**@{*/ +#define __opus_check_encstate_ptr(ptr) ((ptr) + ((ptr) - (OpusEncoder**)(ptr))) +#define __opus_check_decstate_ptr(ptr) ((ptr) + ((ptr) - (OpusDecoder**)(ptr))) +/**@}*/ + +/** These are the actual encoder and decoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +/**@{*/ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST 5120 +#define OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST 5122 +/**@}*/ + +/** @endcond */ + +/** @defgroup opus_multistream_ctls Multistream specific encoder and decoder CTLs + * + * These are convenience macros that are specific to the + * opus_multistream_encoder_ctl() and opus_multistream_decoder_ctl() + * interface. + * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, and + * @ref opus_decoderctls may be applied to a multistream encoder or decoder as + * well. + * In addition, you may retrieve the encoder or decoder state for an specific + * stream via #OPUS_MULTISTREAM_GET_ENCODER_STATE or + * #OPUS_MULTISTREAM_GET_DECODER_STATE and apply CTLs to it individually. + */ +/**@{*/ + +/** Gets the encoder state for an individual stream of a multistream encoder. + * @param[in] x opus_int32: The index of the stream whose encoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the encoder. + * @param[out] y OpusEncoder**: Returns a pointer to the given + * encoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE(x,y) OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, __opus_check_int(x), __opus_check_encstate_ptr(y) + +/** Gets the decoder state for an individual stream of a multistream decoder. + * @param[in] x opus_int32: The index of the stream whose decoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the decoder. + * @param[out] y OpusDecoder**: Returns a pointer to the given + * decoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_DECODER_STATE(x,y) OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST, __opus_check_int(x), __opus_check_decstate_ptr(y) + +/**@}*/ + +/** @defgroup opus_multistream Opus Multistream API + * @{ + * + * The multistream API allows individual Opus streams to be combined into a + * single packet, enabling support for up to 255 channels. Unlike an + * elementary Opus stream, the encoder and decoder must negotiate the channel + * configuration before the decoder can successfully interpret the data in the + * packets produced by the encoder. Some basic information, such as packet + * duration, can be computed without any special negotiation. + * + * The format for multistream Opus packets is defined in + * RFC 7845 + * and is based on the self-delimited Opus framing described in Appendix B of + * RFC 6716. + * Normal Opus packets are just a degenerate case of multistream Opus packets, + * and can be encoded or decoded with the multistream API by setting + * streams to 1 when initializing the encoder or + * decoder. + * + * Multistream Opus streams can contain up to 255 elementary Opus streams. + * These may be either "uncoupled" or "coupled", indicating that the decoder + * is configured to decode them to either 1 or 2 channels, respectively. + * The streams are ordered so that all coupled streams appear at the + * beginning. + * + * A mapping table defines which decoded channel i + * should be used for each input/output (I/O) channel j. This table is + * typically provided as an unsigned char array. + * Let i = mapping[j] be the index for I/O channel j. + * If i < 2*coupled_streams, then I/O channel j is + * encoded as the left channel of stream (i/2) if i + * is even, or as the right channel of stream (i/2) if + * i is odd. Otherwise, I/O channel j is encoded as + * mono in stream (i - coupled_streams), unless it has the special + * value 255, in which case it is omitted from the encoding entirely (the + * decoder will reproduce it as silence). Each value i must either + * be the special value 255 or be less than streams + coupled_streams. + * + * The output channels specified by the encoder + * should use the + * Vorbis + * channel ordering. A decoder may wish to apply an additional permutation + * to the mapping the encoder used to achieve a different output channel + * order (e.g. for outputing in WAV order). + * + * Each multistream packet contains an Opus packet for each stream, and all of + * the Opus packets in a single multistream packet must have the same + * duration. Therefore the duration of a multistream packet can be extracted + * from the TOC sequence of the first stream, which is located at the + * beginning of the packet, just like an elementary Opus stream: + * + * @code + * int nb_samples; + * int nb_frames; + * nb_frames = opus_packet_get_nb_frames(data, len); + * if (nb_frames < 1) + * return nb_frames; + * nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames; + * @endcode + * + * The general encoding and decoding process proceeds exactly the same as in + * the normal @ref opus_encoder and @ref opus_decoder APIs. + * See their documentation for an overview of how to use the corresponding + * multistream functions. + */ + +/** Opus multistream encoder state. + * This contains the complete state of a multistream Opus encoder. + * It is position independent and can be freely copied. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_init + */ +typedef struct OpusMSEncoder OpusMSEncoder; + +/** Opus multistream decoder state. + * This contains the complete state of a multistream Opus decoder. + * It is position independent and can be freely copied. + * @see opus_multistream_decoder_create + * @see opus_multistream_decoder_init + */ +typedef struct OpusMSDecoder OpusMSDecoder; + +/**\name Multistream encoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSEncoder structure. + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size( + int streams, + int coupled_streams +); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a multistream encoder state. + * Call opus_multistream_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); + +/** Initialize a previously allocated multistream encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +OPUS_EXPORT int opus_multistream_surround_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6) OPUS_ARG_NONNULL(7); + +/** Encodes a multistream Opus frame. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode( + OpusMSEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a multistream Opus frame from floating point input. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode_float( + OpusMSEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusMSEncoder allocated by + * opus_multistream_encoder_create(). + * @param st OpusMSEncoder*: Multistream encoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st); + +/** Perform a CTL function on a multistream Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +/**\name Multistream decoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSDecoder structure. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size( + int streams, + int coupled_streams +); + +/** Allocates and initializes a multistream decoder state. + * Call opus_multistream_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int *error +) OPUS_ARG_NONNULL(5); + +/** Intialize a previously allocated decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_decoder_create + * @see opus_multistream_deocder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_decoder_init( + OpusMSDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Decode a multistream Opus packet. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode a multistream Opus packet with floating point output. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on a multistream Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusMSDecoder allocated by + * opus_multistream_decoder_create(). + * @param st OpusMSDecoder: Multistream decoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st); + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_MULTISTREAM_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_projection.h b/library/opusencoder/src/main/cpp/opus/include/opus_projection.h new file mode 100644 index 0000000000..9dabf4e85c --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus_projection.h @@ -0,0 +1,568 @@ +/* Copyright (c) 2017 Google Inc. + Written by Andrew Allen */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_projection.h + * @brief Opus projection reference API + */ + +#ifndef OPUS_PROJECTION_H +#define OPUS_PROJECTION_H + +#include "opus_multistream.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond OPUS_INTERNAL_DOC */ + +/** These are the actual encoder and decoder CTL ID numbers. + * They should not be used directly by applications.c + * In general, SETs should be even and GETs should be odd.*/ +/**@{*/ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST 6001 +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST 6003 +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST 6005 +/**@}*/ + + +/** @endcond */ + +/** @defgroup opus_projection_ctls Projection specific encoder and decoder CTLs + * + * These are convenience macros that are specific to the + * opus_projection_encoder_ctl() and opus_projection_decoder_ctl() + * interface. + * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, + * @ref opus_decoderctls, and @ref opus_multistream_ctls may be applied to a + * projection encoder or decoder as well. + */ +/**@{*/ + +/** Gets the gain (in dB. S7.8-format) of the demixing matrix from the encoder. + * @param[out] x opus_int32 *: Returns the gain (in dB. S7.8-format) + * of the demixing matrix. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST, __opus_check_int_ptr(x) + + +/** Gets the size in bytes of the demixing matrix from the encoder. + * @param[out] x opus_int32 *: Returns the size in bytes of the + * demixing matrix. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST, __opus_check_int_ptr(x) + + +/** Copies the demixing matrix to the supplied pointer location. + * @param[out] x unsigned char *: Returns the demixing matrix to the + * supplied pointer location. + * @param y opus_int32: The size in bytes of the reserved memory at the + * pointer location. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX(x,y) OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, x, __opus_check_int(y) + + +/**@}*/ + +/** Opus projection encoder state. + * This contains the complete state of a projection Opus encoder. + * It is position independent and can be freely copied. + * @see opus_projection_ambisonics_encoder_create + */ +typedef struct OpusProjectionEncoder OpusProjectionEncoder; + + +/** Opus projection decoder state. + * This contains the complete state of a projection Opus decoder. + * It is position independent and can be freely copied. + * @see opus_projection_decoder_create + * @see opus_projection_decoder_init + */ +typedef struct OpusProjectionDecoder OpusProjectionDecoder; + + +/**\name Projection encoder functions */ +/**@{*/ + +/** Gets the size of an OpusProjectionEncoder structure. + * @param channels int: The total number of input channels to encode. + * This must be no more than 255. + * @param mapping_family int: The mapping family to use for selecting + * the appropriate projection. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_ambisonics_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a projection encoder state. + * Call opus_projection_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param mapping_family int: The mapping family to use for selecting + * the appropriate projection. + * @param[out] streams int *: The total number of streams that will + * be encoded from the input. + * @param[out] coupled_streams int *: Number of coupled (2 channel) + * streams that will be encoded from the input. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionEncoder *opus_projection_ambisonics_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + int application, + int *error +) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5); + + +/** Initialize a previously allocated projection encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_projection_ambisonics_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_projection_ambisonics_encoder_create + * @see opus_projection_ambisonics_encoder_get_size + * @param st OpusProjectionEncoder*: Projection encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_projection_ambisonics_encoder_init( + OpusProjectionEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); + + +/** Encodes a projection Opus frame. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode( + OpusProjectionEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + + +/** Encodes a projection Opus frame from floating point input. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode_float( + OpusProjectionEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + + +/** Frees an OpusProjectionEncoder allocated by + * opus_projection_ambisonics_encoder_create(). + * @param st OpusProjectionEncoder*: Projection encoder state to be freed. + */ +OPUS_EXPORT void opus_projection_encoder_destroy(OpusProjectionEncoder *st); + + +/** Perform a CTL function on a projection Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, @ref opus_multistream_ctls, or + * @ref opus_projection_ctls + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + * @see opus_projection_ctls + */ +OPUS_EXPORT int opus_projection_encoder_ctl(OpusProjectionEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + + +/**@}*/ + +/**\name Projection decoder functions */ +/**@{*/ + +/** Gets the size of an OpusProjectionDecoder structure. + * @param channels int: The total number of output channels. + * This must be no more than 255. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_decoder_get_size( + int channels, + int streams, + int coupled_streams +); + + +/** Allocates and initializes a projection decoder state. + * Call opus_projection_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix + * that mapping from coded channels to output channels, + * as described in @ref opus_projection and + * @ref opus_projection_ctls. + * @param demixing_matrix_size opus_int32: The size in bytes of the + * demixing matrix, as + * described in @ref + * opus_projection_ctls. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionDecoder *opus_projection_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + unsigned char *demixing_matrix, + opus_int32 demixing_matrix_size, + int *error +) OPUS_ARG_NONNULL(5); + + +/** Intialize a previously allocated projection decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_projection_decoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_projection_decoder_create + * @see opus_projection_deocder_get_size + * @param st OpusProjectionDecoder*: Projection encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix + * that mapping from coded channels to output channels, + * as described in @ref opus_projection and + * @ref opus_projection_ctls. + * @param demixing_matrix_size opus_int32: The size in bytes of the + * demixing matrix, as + * described in @ref + * opus_projection_ctls. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_projection_decoder_init( + OpusProjectionDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + unsigned char *demixing_matrix, + opus_int32 demixing_matrix_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + + +/** Decode a projection Opus packet. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode( + OpusProjectionDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + + +/** Decode a projection Opus packet with floating point output. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode_float( + OpusProjectionDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + + +/** Perform a CTL function on a projection Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, @ref opus_multistream_ctls, or + * @ref opus_projection_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + * @see opus_projection_ctls + */ +OPUS_EXPORT int opus_projection_decoder_ctl(OpusProjectionDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + + +/** Frees an OpusProjectionDecoder allocated by + * opus_projection_decoder_create(). + * @param st OpusProjectionDecoder: Projection decoder state to be freed. + */ +OPUS_EXPORT void opus_projection_decoder_destroy(OpusProjectionDecoder *st); + + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_PROJECTION_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_types.h b/library/opusencoder/src/main/cpp/opus/include/opus_types.h new file mode 100644 index 0000000000..7cf675580f --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opus_types.h @@ -0,0 +1,166 @@ +/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ +/* Modified by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +/* opus_types.h based on ogg_types.h from libogg */ + +/** + @file opus_types.h + @brief Opus reference implementation types +*/ +#ifndef OPUS_TYPES_H +#define OPUS_TYPES_H + +#define opus_int int /* used for counters etc; at least 16 bits */ +#define opus_int64 long long +#define opus_int8 signed char + +#define opus_uint unsigned int /* used for counters etc; at least 16 bits */ +#define opus_uint64 unsigned long long +#define opus_uint8 unsigned char + +/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ +#if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) +#include +# undef opus_int64 +# undef opus_int8 +# undef opus_uint64 +# undef opus_uint8 + typedef int8_t opus_int8; + typedef uint8_t opus_uint8; + typedef int16_t opus_int16; + typedef uint16_t opus_uint16; + typedef int32_t opus_int32; + typedef uint32_t opus_uint32; + typedef int64_t opus_int64; + typedef uint64_t opus_uint64; +#elif defined(_WIN32) + +# if defined(__CYGWIN__) +# include <_G_config.h> + typedef _G_int32_t opus_int32; + typedef _G_uint32_t opus_uint32; + typedef _G_int16 opus_int16; + typedef _G_uint16 opus_uint16; +# elif defined(__MINGW32__) + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; +# elif defined(__MWERKS__) + typedef int opus_int32; + typedef unsigned int opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; +# else + /* MSVC/Borland */ + typedef __int32 opus_int32; + typedef unsigned __int32 opus_uint32; + typedef __int16 opus_int16; + typedef unsigned __int16 opus_uint16; +# endif + +#elif defined(__MACOS__) + +# include + typedef SInt16 opus_int16; + typedef UInt16 opus_uint16; + typedef SInt32 opus_int32; + typedef UInt32 opus_uint32; + +#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ + +# include + typedef int16_t opus_int16; + typedef u_int16_t opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined(__BEOS__) + + /* Be */ +# include + typedef int16 opus_int16; + typedef u_int16 opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined (__EMX__) + + /* OS/2 GCC */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined (DJGPP) + + /* DJGPP */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(R5900) + + /* PS2 EE */ + typedef int opus_int32; + typedef unsigned opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; + +#elif defined(__SYMBIAN32__) + + /* Symbian GCC */ + typedef signed short opus_int16; + typedef unsigned short opus_uint16; + typedef signed int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef long opus_int32; + typedef unsigned long opus_uint32; + +#elif defined(CONFIG_TI_C6X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#else + + /* Give up, take a reasonable guess */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#endif + +#endif /* OPUS_TYPES_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opusenc.h b/library/opusencoder/src/main/cpp/opus/include/opusenc.h new file mode 100644 index 0000000000..ca1a5efe8e --- /dev/null +++ b/library/opusencoder/src/main/cpp/opus/include/opusenc.h @@ -0,0 +1,358 @@ +/* Copyright (c) 2017 Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(_opusenc_h) +# define _opusenc_h (1) + +/**\mainpage + \section Introduction + + This is the documentation for the libopusenc C API. + + The libopusenc package provides a convenient high-level API for + encoding Ogg Opus files. + + \section Organization + + The main API is divided into several sections: + - \ref encoding + - \ref comments + - \ref encoder_ctl + - \ref callbacks + - \ref error_codes + + \section Overview + + The libopusfile API provides an easy way to encode Ogg Opus files using + libopus. +*/ + +# if defined(__cplusplus) +extern "C" { +# endif + +#include + +#ifndef OPE_EXPORT +# if defined(WIN32) +# if defined(OPE_BUILD) && defined(DLL_EXPORT) +# define OPE_EXPORT __declspec(dllexport) +# else +# define OPE_EXPORT +# endif +# elif defined(__GNUC__) && defined(OPE_BUILD) +# define OPE_EXPORT __attribute__ ((visibility ("default"))) +# else +# define OPE_EXPORT +# endif +#endif + +/**\defgroup error_codes Error Codes*/ +/*@{*/ +/**\name List of possible error codes + Many of the functions in this library return a negative error code when a + function fails. + This list provides a brief explanation of the common errors. + See each individual function for more details on what a specific error code + means in that context.*/ +/*@{*/ + + +/* Bump this when we change the API. */ +/** API version for this header. Can be used to check for features at compile time. */ +#define OPE_API_VERSION 0 + +#define OPE_OK 0 +/* Based on the relevant libopus code minus 10. */ +#define OPE_BAD_ARG -11 +#define OPE_INTERNAL_ERROR -13 +#define OPE_UNIMPLEMENTED -15 +#define OPE_ALLOC_FAIL -17 + +/* Specific to libopusenc. */ +#define OPE_CANNOT_OPEN -30 +#define OPE_TOO_LATE -31 +#define OPE_UNRECOVERABLE -32 +#define OPE_INVALID_PICTURE -33 +#define OPE_INVALID_ICON -34 +/*@}*/ +/*@}*/ + + +/* These are the "raw" request values -- they should usually not be used. */ +#define OPE_SET_DECISION_DELAY_REQUEST 14000 +#define OPE_GET_DECISION_DELAY_REQUEST 14001 +#define OPE_SET_MUXING_DELAY_REQUEST 14002 +#define OPE_GET_MUXING_DELAY_REQUEST 14003 +#define OPE_SET_COMMENT_PADDING_REQUEST 14004 +#define OPE_GET_COMMENT_PADDING_REQUEST 14005 +#define OPE_SET_SERIALNO_REQUEST 14006 +#define OPE_GET_SERIALNO_REQUEST 14007 +#define OPE_SET_PACKET_CALLBACK_REQUEST 14008 +/*#define OPE_GET_PACKET_CALLBACK_REQUEST 14009*/ +#define OPE_SET_HEADER_GAIN_REQUEST 14010 +#define OPE_GET_HEADER_GAIN_REQUEST 14011 + +/**\defgroup encoder_ctl Encoding Options*/ +/*@{*/ + +/**\name Control parameters + + Macros for setting encoder options.*/ +/*@{*/ + +#define OPE_SET_DECISION_DELAY(x) OPE_SET_DECISION_DELAY_REQUEST, __opus_check_int(x) +#define OPE_GET_DECISION_DELAY(x) OPE_GET_DECISION_DELAY_REQUEST, __opus_check_int_ptr(x) +#define OPE_SET_MUXING_DELAY(x) OPE_SET_MUXING_DELAY_REQUEST, __opus_check_int(x) +#define OPE_GET_MUXING_DELAY(x) OPE_GET_MUXING_DELAY_REQUEST, __opus_check_int_ptr(x) +#define OPE_SET_COMMENT_PADDING(x) OPE_SET_COMMENT_PADDING_REQUEST, __opus_check_int(x) +#define OPE_GET_COMMENT_PADDING(x) OPE_GET_COMMENT_PADDING_REQUEST, __opus_check_int_ptr(x) +#define OPE_SET_SERIALNO(x) OPE_SET_SERIALNO_REQUEST, __opus_check_int(x) +#define OPE_GET_SERIALNO(x) OPE_GET_SERIALNO_REQUEST, __opus_check_int_ptr(x) +/* FIXME: Add type-checking macros to these. */ +#define OPE_SET_PACKET_CALLBACK(x,u) OPE_SET_PACKET_CALLBACK_REQUEST, (x), (u) +/*#define OPE_GET_PACKET_CALLBACK(x,u) OPE_GET_PACKET_CALLBACK_REQUEST, (x), (u)*/ +#define OPE_SET_HEADER_GAIN(x,u) OPE_SET_HEADER_GAIN_REQUEST, __opus_check_int(x) +#define OPE_GET_HEADER_GAIN(x,u) OPE_GET_HEADER_GAIN_REQUEST, __opus_check_int_ptr(x) +/*@}*/ +/*@}*/ + +/**\defgroup callbacks Callback Functions */ +/*@{*/ + +/**\name Callback functions + + These are the callbacks that can be implemented for an encoder.*/ +/*@{*/ + +/** Called for writing a page. */ +typedef int (*ope_write_func)(void *user_data, const unsigned char *ptr, opus_int32 len); + +/** Called for closing a stream. */ +typedef int (*ope_close_func)(void *user_data); + +/** Called on every packet encoded (including header). */ +typedef int (*ope_packet_func)(void *user_data, const unsigned char *packet_ptr, opus_int32 packet_len, opus_uint32 flags); + +/** Callback functions for accessing the stream. */ +typedef struct { + /** Callback for writing to the stream. */ + ope_write_func write; + /** Callback for closing the stream. */ + ope_close_func close; +} OpusEncCallbacks; +/*@}*/ +/*@}*/ + +/** Opaque comments struct. */ +typedef struct OggOpusComments OggOpusComments; + +/** Opaque encoder struct. */ +typedef struct OggOpusEnc OggOpusEnc; + +/**\defgroup comments Comments Handling */ +/*@{*/ + +/**\name Functions for handling comments + + These functions make it possible to add comments and pictures to Ogg Opus files.*/ +/*@{*/ + +/** Create a new comments object. + \return Newly-created comments object. */ +OPE_EXPORT OggOpusComments *ope_comments_create(void); + +/** Create a deep copy of a comments object. + \param comments Comments object to copy + \return Deep copy of input. */ +OPE_EXPORT OggOpusComments *ope_comments_copy(OggOpusComments *comments); + +/** Destroys a comments object. + \param comments Comments object to destroy*/ +OPE_EXPORT void ope_comments_destroy(OggOpusComments *comments); + +/** Add a comment. + \param[in,out] comments Where to add the comments + \param tag Tag for the comment (must not contain = char) + \param val Value for the tag + \return Error code + */ +OPE_EXPORT int ope_comments_add(OggOpusComments *comments, const char *tag, const char *val); + +/** Add a comment as a single tag=value string. + \param[in,out] comments Where to add the comments + \param tag_and_val string of the form tag=value (must contain = char) + \return Error code + */ +OPE_EXPORT int ope_comments_add_string(OggOpusComments *comments, const char *tag_and_val); + +/** Add a picture. + \param[in,out] comments Where to add the comments + \param filename File name for the picture + \param picture_type Type of picture (-1 for default) + \param description Description (NULL means no comment) + \return Error code + */ +OPE_EXPORT int ope_comments_add_picture(OggOpusComments *comments, const char *filename, int picture_type, const char *description); + +/*@}*/ +/*@}*/ + +/**\defgroup encoding Encoding */ +/*@{*/ + +/**\name Functions for encoding Ogg Opus files + + These functions make it possible to encode Ogg Opus files.*/ +/*@{*/ + +/** Create a new OggOpus file. + \param path Path where to create the file + \param comments Comments associated with the stream + \param rate Input sampling rate (48 kHz is faster) + \param channels Number of channels + \param family Mapping family (0 for mono/stereo, 1 for surround) + \param[out] error Error code (NULL if no error is to be returned) + \return Newly-created encoder. + */ +OPE_EXPORT OggOpusEnc *ope_encoder_create_file(const char *path, OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); + +/** Create a new OggOpus stream to be handled using callbacks + \param callbacks Callback functions + \param user_data Pointer to be associated with the stream and passed to the callbacks + \param comments Comments associated with the stream + \param rate Input sampling rate (48 kHz is faster) + \param channels Number of channels + \param family Mapping family (0 for mono/stereo, 1 for surround) + \param[out] error Error code (NULL if no error is to be returned) + \return Newly-created encoder. + */ +OPE_EXPORT OggOpusEnc *ope_encoder_create_callbacks(const OpusEncCallbacks *callbacks, void *user_data, + OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); + +/** Create a new OggOpus stream to be used along with.ope_encoder_get_page(). + This is mostly useful for muxing with other streams. + \param comments Comments associated with the stream + \param rate Input sampling rate (48 kHz is faster) + \param channels Number of channels + \param family Mapping family (0 for mono/stereo, 1 for surround) + \param[out] error Error code (NULL if no error is to be returned) + \return Newly-created encoder. + */ +OPE_EXPORT OggOpusEnc *ope_encoder_create_pull(OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); + +/** Add/encode any number of float samples to the stream. + \param[in,out] enc Encoder + \param pcm Floating-point PCM values in the +/-1 range (interleaved if multiple channels) + \param samples_per_channel Number of samples for each channel + \return Error code*/ +OPE_EXPORT int ope_encoder_write_float(OggOpusEnc *enc, const float *pcm, int samples_per_channel); + +/** Add/encode any number of 16-bit linear samples to the stream. + \param[in,out] enc Encoder + \param pcm Linear 16-bit PCM values in the [-32768,32767] range (interleaved if multiple channels) + \param samples_per_channel Number of samples for each channel + \return Error code*/ +OPE_EXPORT int ope_encoder_write(OggOpusEnc *enc, const opus_int16 *pcm, int samples_per_channel); + +/** Get the next page from the stream (only if using ope_encoder_create_pull()). + \param[in,out] enc Encoder + \param[out] page Next available encoded page + \param[out] len Size (in bytes) of the page returned + \param flush If non-zero, forces a flush of the page (if any data avaiable) + \return 1 if there is a page available, 0 if not. */ +OPE_EXPORT int ope_encoder_get_page(OggOpusEnc *enc, unsigned char **page, opus_int32 *len, int flush); + +/** Finalizes the stream, but does not deallocate the object. + \param[in,out] enc Encoder + \return Error code + */ +OPE_EXPORT int ope_encoder_drain(OggOpusEnc *enc); + +/** Deallocates the obect. Make sure to ope_drain() first. + \param[in,out] enc Encoder + */ +OPE_EXPORT void ope_encoder_destroy(OggOpusEnc *enc); + +/** Ends the stream and create a new stream within the same file. + \param[in,out] enc Encoder + \param comments Comments associated with the stream + \return Error code + */ +OPE_EXPORT int ope_encoder_chain_current(OggOpusEnc *enc, OggOpusComments *comments); + +/** Ends the stream and create a new file. + \param[in,out] enc Encoder + \param path Path where to write the new file + \param comments Comments associated with the stream + \return Error code + */ +OPE_EXPORT int ope_encoder_continue_new_file(OggOpusEnc *enc, const char *path, OggOpusComments *comments); + +/** Ends the stream and create a new file (callback-based). + \param[in,out] enc Encoder + \param user_data Pointer to be associated with the new stream and passed to the callbacks + \param comments Comments associated with the stream + \return Error code + */ +OPE_EXPORT int ope_encoder_continue_new_callbacks(OggOpusEnc *enc, void *user_data, OggOpusComments *comments); + +/** Write out the header now rather than wait for audio to begin. + \param[in,out] enc Encoder + \return Error code + */ +OPE_EXPORT int ope_encoder_flush_header(OggOpusEnc *enc); + +/** Sets encoder options. + \param[in,out] enc Encoder + \param request Use a request macro + \return Error code + */ +OPE_EXPORT int ope_encoder_ctl(OggOpusEnc *enc, int request, ...); + +/** Converts a libopusenc error code into a human readable string. + * + * @param error Error number + * @returns Error string + */ +OPE_EXPORT const char *ope_strerror(int error); + +/** Returns a string representing the version of libopusenc being used at run time. + \return A string describing the version of this library */ +OPE_EXPORT const char *ope_get_version_string(void); + +/** ABI version for this header. Can be used to check for features at run time. + \return An integer representing the ABI version */ +OPE_EXPORT int ope_get_abi_version(void); + +/*@}*/ +/*@}*/ + +# if defined(__cplusplus) +} +# endif + +#endif diff --git a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so new file mode 100755 index 0000000000000000000000000000000000000000..466c1df13ae33ebef9ba62a6f7caf35ec62ef771 GIT binary patch literal 318136 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%0DRz)#>N;9HsvNau*pRwiUKn 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 literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so new file mode 100755 index 0000000000000000000000000000000000000000..9389efd9199c23332edceacd1ca5528f90d528b4 GIT binary patch literal 49448 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

-LJ5Va)V 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⪻%S0-`bbE;@{wG(w`U>G$*5(nw;)0 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

@r5{|A)5CtC0dDwtXwqF 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 Ld<5Vb@%O(0r{|su literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so new file mode 100755 index 0000000000000000000000000000000000000000..655f296d90a6d8da70387bdefbf54ece5aa13688 GIT binary patch literal 39836 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 literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so new file mode 100755 index 0000000000000000000000000000000000000000..0acf14474b50e79f44bacedc092575ac3ea9fa15 GIT binary patch literal 329156 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�NQ7PcEW=8M1l6t1_o|Jd$9u*qXwn^quPga3>yNy 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%0AX|AgYSm@Y1KjZR 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 literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so new file mode 100755 index 0000000000000000000000000000000000000000..f862ea15f7f6062346f562f2794e1ca798e89eb0 GIT binary patch literal 56796 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 literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so new file mode 100755 index 0000000000000000000000000000000000000000..1480c8c586fab9df0bf3baebfa7454029531f978 GIT binary patch literal 346688 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%0D}yAbuigF??pM;n^e3ExCU`O70chpTqs+ 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 jE~IrMlX5jYs3~dPNORvB%f3UwiMHrZGDV@R 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 literal 0 HcmV?d00001 diff --git a/library/opusencoder/src/main/cpp/opuscodec.cpp b/library/opusencoder/src/main/cpp/opuscodec.cpp new file mode 100644 index 0000000000..32e4c07591 --- /dev/null +++ b/library/opusencoder/src/main/cpp/opuscodec.cpp @@ -0,0 +1,26 @@ +#include +#include "codec/CodecOggOpus.h" + +CodecOggOpus oggCodec; + +extern "C" +JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_init(JNIEnv *env, jobject thiz, jstring file_path, jint sample_rate) { + char *path = (char*) env->GetStringUTFChars(file_path, 0); + return oggCodec.encoderInit(path, sample_rate); +} + +extern "C" +JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_writeFrame(JNIEnv *env, jobject thiz, jshortArray shorts, jint samples_per_channel) { + jshort *nativeShorts = env->GetShortArrayElements(shorts, 0); + return oggCodec.writeFrame((short *) nativeShorts, samples_per_channel); +} + +extern "C" +JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_setBitrate(JNIEnv *env, jobject thiz, jint bitrate) { + return oggCodec.setBitrate(bitrate); +} + +extern "C" +JNIEXPORT void JNICALL Java_im_vector_opusencoder_OggOpusEncoder_encoderRelease(JNIEnv *env, jobject thiz) { + oggCodec.encoderRelease(); +} diff --git a/library/opusencoder/src/main/cpp/utils/Logger.h b/library/opusencoder/src/main/cpp/utils/Logger.h new file mode 100644 index 0000000000..9efdc51d41 --- /dev/null +++ b/library/opusencoder/src/main/cpp/utils/Logger.h @@ -0,0 +1,11 @@ +#ifndef ANDROIDOPUSENCODER_LOGGER_H +#define ANDROIDOPUSENCODER_LOGGER_H +#include + + +#define LOGE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__) +#define LOGW(tag, ...) __android_log_print(ANDROID_LOG_WARN, tag, __VA_ARGS__) +#define LOGI(tag, ...) __android_log_print(ANDROID_LOG_INFO, tag, __VA_ARGS__) +#define LOGD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__) + +#endif //ANDROIDOPUSENCODER_LOGGER_H diff --git a/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt b/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt new file mode 100644 index 0000000000..8f035e1660 --- /dev/null +++ b/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt @@ -0,0 +1,54 @@ +/* + * 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.opusencoder + +import android.util.Log +import androidx.annotation.IntRange +import im.vector.opusencoder.configuration.SampleRate + +class OggOpusEncoder { + + companion object { + + private const val TAG = "OggOpusEncoder" + + init { + try { + System.loadLibrary("opuscodec") + } catch (e: Exception) { + Log.e(TAG, "Couldn't load opus library: $e") + } + } + } + + fun init(filePath: String, sampleRate: SampleRate): Int { + return init(filePath, sampleRate.value) + } + private external fun init(filePath: String, sampleRate: Int): Int + + external fun setBitrate(@IntRange(from = 500, to = 512000) bitrate: Int): Int + + fun encode(shorts: ShortArray, samplesPerChannel: Int): Int { + return writeFrame(shorts, samplesPerChannel) + } + private external fun writeFrame(shorts: ShortArray, samplesPerChannel: Int): Int + + fun release() { + encoderRelease() + } + private external fun encoderRelease() +} diff --git a/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt b/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt new file mode 100644 index 0000000000..70f8590863 --- /dev/null +++ b/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt @@ -0,0 +1,28 @@ +/* + * 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.opusencoder.configuration + +/** + * Sampling rate of the input signal in Hz. + */ +sealed class SampleRate private constructor(val value: Int) { + object _8khz : SampleRate(8000) + object _12kHz : SampleRate(12000) + object _16kHz : SampleRate(16000) + object _24KHz : SampleRate(24000) + object _48kHz : SampleRate(48000) +} diff --git a/settings.gradle b/settings.gradle index 782d2caf4a..0f537ed48a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,4 @@ include ':library:attachment-viewer' include ':library:diff-match-patch' include ':library:multipicker' include ':matrix-sdk-android-flow' +include ':library:opusencoder' diff --git a/vector/build.gradle b/vector/build.gradle index ae909bf513..7aa7ba7366 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -337,6 +337,9 @@ android { } dependencies { + implementation fileTree(dir: "libs", include: ["*.aar", "*.jar"]) + implementation project(':library:opusencoder') + implementation project(":vector-config") implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-flow") @@ -427,9 +430,6 @@ dependencies { // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.7.0' - // To convert voice message on old platforms. Always keep the LTS suffix! - implementation 'com.arthenica:ffmpeg-kit-audio:4.5.1.LTS' - // Alerter implementation 'com.github.tapadoo:alerter:7.2.4' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 8f27776fbf..c4f0bf0023 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -631,18 +631,25 @@ Apache License -

- GNU GENERAL PUBLIC LICENSE -
- Version 3, 29 June 2007 -

  • - ffmpeg-kit + Opus
    - Copyright (c) 2021 Taner Sener + Copyright (c) 1994-2013 Xiph.Org Foundation and contributors + Copyright (c) 2017 Jean-Marc Valin
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+- Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 98694d9c9e..9374d51228 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -77,17 +77,13 @@ class AudioMessageHelper @Inject constructor( startRecordingAmplitudes() } - fun stopRecording(convertForSending: Boolean): MultiPickerAudioType? { + fun stopRecording(): MultiPickerAudioType? { tryOrNull("Cannot stop media recording amplitude") { stopRecordingAmplitudes() } val voiceMessageFile = tryOrNull("Cannot stop media recorder!") { voiceRecorder.stopRecord() - if (convertForSending) { - voiceRecorder.getVoiceMessageFile() - } else { - voiceRecorder.getCurrentRecord() - } + voiceRecorder.getVoiceMessageFile() } try { @@ -127,7 +123,7 @@ class AudioMessageHelper @Inject constructor( } fun startOrPauseRecordingPlayback() { - voiceRecorder.getCurrentRecord()?.let { + voiceRecorder.getVoiceMessageFile()?.let { startOrPausePlayback(AudioMessagePlaybackTracker.RECORDING_ID, it) } } @@ -260,7 +256,7 @@ class AudioMessageHelper @Inject constructor( } fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? { - val audioType = stopRecording(convertForSending = false) + val audioType = stopRecording() stopPlayback() if (deleteRecord) { deleteRecording() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 53f89603ff..37b17d9133 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -39,7 +39,6 @@ import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.voice.VoicePlayerHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -80,7 +79,6 @@ class MessageComposerViewModel @AssistedInject constructor( private val rainbowGenerator: RainbowGenerator, private val audioMessageHelper: AudioMessageHelper, private val analyticsTracker: AnalyticsTracker, - private val voicePlayerHelper: VoicePlayerHelper ) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! @@ -856,7 +854,7 @@ class MessageComposerViewModel @AssistedInject constructor( if (isCancelled) { audioMessageHelper.deleteRecording() } else { - audioMessageHelper.stopRecording(convertForSending = true)?.let { audioType -> + audioMessageHelper.stopRecording()?.let { audioType -> if (audioType.duration > 1000) { room.sendService().sendMedia( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), @@ -877,10 +875,8 @@ class MessageComposerViewModel @AssistedInject constructor( try { // Download can fail val audioFile = session.fileService().downloadFile(action.messageAudioContent) - // Conversion can fail, fallback to the original file in this case and let the player fail for us - val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile // Play can fail - audioMessageHelper.startOrPausePlayback(action.eventId, convertedFile) + audioMessageHelper.startOrPausePlayback(action.eventId, audioFile) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) } diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt index 14bf09c6c4..13b8149f83 100644 --- a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -18,7 +18,6 @@ package im.vector.app.features.voice import android.content.Context import android.media.MediaRecorder -import android.net.Uri import android.os.Build import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.util.md5 @@ -28,19 +27,14 @@ import java.util.UUID abstract class AbstractVoiceRecorder( private val context: Context, - private val filenameExt: String + internal val filenameExt: String ) : VoiceRecorder { - private val outputDirectory: File by lazy { - File(context.cacheDir, "voice_records").also { - it.mkdirs() - } - } + private val outputDirectory: File by lazy { ensureAudioDirectory(context) } private var mediaRecorder: MediaRecorder? = null private var outputFile: File? = null abstract fun setOutputFormat(mediaRecorder: MediaRecorder) - abstract fun convertFile(recordedFile: File?): File? private fun init() { createMediaRecorder().let { @@ -104,19 +98,7 @@ abstract class AbstractVoiceRecorder( return mediaRecorder?.maxAmplitude ?: 0 } - override fun getCurrentRecord(): File? { + override fun getVoiceMessageFile(): File? { return outputFile } - - override fun getVoiceMessageFile(): File? { - return convertFile(outputFile) - } -} - -private fun ContentAttachmentData.findVoiceFile(baseDirectory: File): File { - return File(baseDirectory, queryUri.takePathAfter(baseDirectory.name)) -} - -private fun Uri.takePathAfter(after: String): String { - return pathSegments.takeLastWhile { it != after }.joinToString(separator = "/") { it } } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt deleted file mode 100644 index 93531bcc2f..0000000000 --- a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.voice - -import android.content.Context -import android.os.Build -import com.arthenica.ffmpegkit.FFmpegKit -import com.arthenica.ffmpegkit.ReturnCode -import im.vector.app.core.time.Clock -import timber.log.Timber -import java.io.File -import javax.inject.Inject - -class VoicePlayerHelper @Inject constructor( - private val clock: Clock, - context: Context -) { - private val outputDirectory: File by lazy { - File(context.cacheDir, "voice_records").also { - it.mkdirs() - } - } - - /** - * Ensure the file is encoded using aac audio codec. - */ - fun convertFile(file: File): File? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // Nothing to do - file - } else { - // Convert to mp4 - val targetFile = File(outputDirectory, "Voice.mp4") - if (targetFile.exists()) { - targetFile.delete() - } - val start = clock.epochMillis() - val session = FFmpegKit.execute("-i \"${file.path}\" -c:a aac \"${targetFile.path}\"") - val duration = clock.epochMillis() - start - Timber.d("Convert to mp4 in $duration ms. Size in bytes from ${file.length()} to ${targetFile.length()}") - return when { - ReturnCode.isSuccess(session.returnCode) -> { - // SUCCESS - targetFile - } - ReturnCode.isCancel(session.returnCode) -> { - // CANCEL - null - } - else -> { - // FAILURE - Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}") - // TODO throw? - null - } - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt index a5f4b52982..785fb9b4da 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt @@ -16,6 +16,8 @@ package im.vector.app.features.voice +import android.content.Context +import android.net.Uri import org.matrix.android.sdk.api.session.content.ContentAttachmentData import java.io.File @@ -44,13 +46,25 @@ interface VoiceRecorder { fun getMaxAmplitude(): Int - /** - * Not guaranteed to be a ogg file. - */ - fun getCurrentRecord(): File? - /** * Guaranteed to be a ogg file. */ fun getVoiceMessageFile(): File? } + +/** + * Ensures a voice records directory exists and returns it. + */ +internal fun VoiceRecorder.ensureAudioDirectory(context: Context): File { + return File(context.cacheDir, "voice_records").also { + it.mkdirs() + } +} + +internal fun ContentAttachmentData.findVoiceFile(baseDirectory: File): File { + return File(baseDirectory, queryUri.takePathAfter(baseDirectory.name)) +} + +private fun Uri.takePathAfter(after: String): String { + return pathSegments.takeLastWhile { it != after }.joinToString(separator = "/") { it } +} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index 90a8d8b246..e90df82383 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. @@ -17,55 +17,146 @@ package im.vector.app.features.voice import android.content.Context +import android.media.AudioFormat +import android.media.AudioRecord import android.media.MediaRecorder -import com.arthenica.ffmpegkit.FFmpegKit -import com.arthenica.ffmpegkit.FFmpegKitConfig -import com.arthenica.ffmpegkit.Level -import com.arthenica.ffmpegkit.ReturnCode -import im.vector.app.BuildConfig -import im.vector.app.core.time.Clock -import timber.log.Timber +import android.media.audiofx.AutomaticGainControl +import android.media.audiofx.NoiseSuppressor +import android.os.Build +import im.vector.opusencoder.OggOpusEncoder +import im.vector.opusencoder.configuration.SampleRate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.md5 import java.io.File +import java.util.UUID +import kotlin.coroutines.CoroutineContext +/** + * VoiceRecorder to be used on Android versions < [Build.VERSION_CODES.Q]. It uses libopus to record ogg files. + */ class VoiceRecorderL( context: Context, - private val clock: Clock, -) : AbstractVoiceRecorder(context, "mp4") { - override fun setOutputFormat(mediaRecorder: MediaRecorder) { - // Use AAC/MP4 format here - mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) - mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + coroutineContext: CoroutineContext, +) : VoiceRecorder { + + companion object { + private val SAMPLE_RATE = SampleRate._48kHz + private const val BITRATE = 24 * 1024 } - override fun convertFile(recordedFile: File?): File? { - if (BuildConfig.DEBUG) { - FFmpegKitConfig.setLogLevel(Level.AV_LOG_INFO) - } - recordedFile ?: return null - // Convert to OGG - val targetFile = File(recordedFile.path.removeSuffix("mp4") + "ogg") - if (targetFile.exists()) { - targetFile.delete() - } - val start = clock.epochMillis() - val session = FFmpegKit.execute("-i \"${recordedFile.path}\" -c:a libvorbis \"${targetFile.path}\"") - val duration = clock.epochMillis() - start - Timber.d("Convert to ogg in $duration ms. Size in bytes from ${recordedFile.length()} to ${targetFile.length()}") - return when { - ReturnCode.isSuccess(session.returnCode) -> { - // SUCCESS - targetFile + private val outputDirectory: File by lazy { ensureAudioDirectory(context) } + private var outputFile: File? = null + + private val recorderScope = CoroutineScope(coroutineContext) + private var recordingJob: Job? = null + + private var audioRecorder: AudioRecord? = null + private var noiseSuppressor: NoiseSuppressor? = null + private var automaticGainControl: AutomaticGainControl? = null + private val codec = OggOpusEncoder() + + // Size of the audio buffer for Short values + private var bufferSizeInShorts = 0 + private var maxAmplitude = 0 + + private fun initializeCodec(filePath: String) { + codec.init(filePath, SAMPLE_RATE) + codec.setBitrate(BITRATE) + + createAudioRecord() + + val recorder = this.audioRecorder ?: return + + if (NoiseSuppressor.isAvailable()) { + noiseSuppressor = tryOrNull { + NoiseSuppressor.create(recorder.audioSessionId).also { it.enabled = true } } - ReturnCode.isCancel(session.returnCode) -> { - // CANCEL - null - } - else -> { - // FAILURE - Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}") - // TODO throw? - null + } + + if (AutomaticGainControl.isAvailable()) { + automaticGainControl = tryOrNull { + AutomaticGainControl.create(recorder.audioSessionId).also { it.enabled = true } } } } + + override fun initializeRecord(attachmentData: ContentAttachmentData) { + outputFile = attachmentData.findVoiceFile(outputDirectory) + } + + override fun startRecord(roomId: String) { + val fileName = "${UUID.randomUUID()}.ogg" + val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply { + mkdirs() + } + val outputFile = File(outputDirectoryForRoom, fileName) + this.outputFile = outputFile + + initializeCodec(outputFile.absolutePath) + + recordingJob = recorderScope.launch { + while (audioRecorder?.state != AudioRecord.STATE_INITIALIZED) { + // If the recorder is not ready let's give it some extra time + delay(10L) + } + audioRecorder?.startRecording() + + val buffer = ShortArray(bufferSizeInShorts) + while (isActive) { + val read = audioRecorder?.read(buffer, 0, bufferSizeInShorts) ?: -1 + calculateMaxAmplitude(buffer) + if (read <= 0) continue + codec.encode(buffer, read) + } + } + } + + override fun stopRecord() { + recordingJob?.cancel() + + if (audioRecorder?.state == AudioRecord.STATE_INITIALIZED) { + audioRecorder?.stop() + } + audioRecorder?.release() + + noiseSuppressor?.release() + noiseSuppressor = null + + automaticGainControl?.release() + automaticGainControl = null + + codec.release() + } + + override fun cancelRecord() { + outputFile?.delete() + outputFile = null + } + + override fun getMaxAmplitude(): Int { + return maxAmplitude + } + + override fun getVoiceMessageFile(): File? { + return outputFile + } + + private fun createAudioRecord() { + val channelConfig = AudioFormat.CHANNEL_IN_MONO + val format = AudioFormat.ENCODING_PCM_16BIT + bufferSizeInShorts = AudioRecord.getMinBufferSize(SAMPLE_RATE.value, channelConfig, format) + // Buffer is created as a ShortArray, but AudioRecord needs the size in bytes + val bufferSizeInBytes = bufferSizeInShorts * 2 + audioRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE.value, channelConfig, format, bufferSizeInBytes) + } + + private fun calculateMaxAmplitude(buffer: ShortArray) { + maxAmplitude = buffer.maxOf { it }.toInt() + } } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt index 0cee8f4f6e..d24e7fcc8c 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -18,18 +18,17 @@ package im.vector.app.features.voice import android.content.Context import android.os.Build -import im.vector.app.core.time.Clock +import kotlinx.coroutines.Dispatchers import javax.inject.Inject class VoiceRecorderProvider @Inject constructor( private val context: Context, - private val clock: Clock, ) { fun provideVoiceRecorder(): VoiceRecorder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { VoiceRecorderQ(context) } else { - VoiceRecorderL(context, clock) + VoiceRecorderL(context, Dispatchers.IO) } } } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt index d6f4676893..5091ddfa3b 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt @@ -20,8 +20,10 @@ import android.content.Context import android.media.MediaRecorder import android.os.Build import androidx.annotation.RequiresApi -import java.io.File +/** + * VoiceRecorder to be used on Android versions >= [Build.VERSION_CODES.Q]. It uses the native OPUS support on Android 10+. + */ @RequiresApi(Build.VERSION_CODES.Q) class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") { override fun setOutputFormat(mediaRecorder: MediaRecorder) { @@ -29,9 +31,4 @@ class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") { mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) } - - override fun convertFile(recordedFile: File?): File? { - // Nothing to do here - return recordedFile - } } From fa2d9e90ed530fad6525f01120595b5db0c4172c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 31 May 2022 15:37:54 +0200 Subject: [PATCH 095/314] Fix copyright in C++ files --- .../src/main/cpp/codec/CodecOggOpus.cpp | 18 +++++++++++++++--- .../src/main/cpp/codec/CodecOggOpus.h | 18 +++++++++++++++--- library/opusencoder/src/main/cpp/opuscodec.cpp | 16 ++++++++++++++++ .../app/features/voice/VoiceRecorderL.kt | 1 + 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp index c5f80ec989..3b28780ef0 100644 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp @@ -1,6 +1,18 @@ -// -// Created by Jorge Martín Espinosa on 30/5/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. + */ #include "CodecOggOpus.h" #include "../utils/Logger.h" diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h index 4696a86f64..5c94a0d874 100644 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h @@ -1,6 +1,18 @@ -// -// Created by Jorge Martín Espinosa on 30/5/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. + */ #ifndef ELEMENT_ANDROID_CODECOGGOPUS_H #define ELEMENT_ANDROID_CODECOGGOPUS_H diff --git a/library/opusencoder/src/main/cpp/opuscodec.cpp b/library/opusencoder/src/main/cpp/opuscodec.cpp index 32e4c07591..51bd656c5d 100644 --- a/library/opusencoder/src/main/cpp/opuscodec.cpp +++ b/library/opusencoder/src/main/cpp/opuscodec.cpp @@ -1,3 +1,19 @@ +/* + * 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. + */ + #include #include "codec/CodecOggOpus.h" diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index e90df82383..69de911d60 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -124,6 +124,7 @@ class VoiceRecorderL( audioRecorder?.stop() } audioRecorder?.release() + audioRecorder = null noiseSuppressor?.release() noiseSuppressor = null From b993bd9aef2dbe3a5677c5600521b05e11a512a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 31 May 2022 15:53:05 +0200 Subject: [PATCH 096/314] Fix crash when asking for Mic permissions, stopRecord is called with no codec --- .../java/im/vector/app/features/voice/VoiceRecorderL.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index 69de911d60..8816ffafbd 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -118,12 +118,13 @@ class VoiceRecorderL( } override fun stopRecord() { + val recorder = this.audioRecorder ?: return recordingJob?.cancel() - if (audioRecorder?.state == AudioRecord.STATE_INITIALIZED) { - audioRecorder?.stop() + if (recorder.state == AudioRecord.STATE_INITIALIZED) { + recorder.stop() } - audioRecorder?.release() + recorder.release() audioRecorder = null noiseSuppressor?.release() From 155842f8bcf5ec7a885f8cb43853a6ffdd94d1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 1 Jun 2022 09:06:13 +0200 Subject: [PATCH 097/314] Add some doc comments --- .../opusencoder/src/main/cpp/codec/CodecOggOpus.cpp | 12 ++++++++++-- .../opusencoder/src/main/cpp/codec/CodecOggOpus.h | 11 ++++++++++- .../java/im/vector/opusencoder/OggOpusEncoder.kt | 3 +++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp index 3b28780ef0..d167c69cbf 100644 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp @@ -20,9 +20,13 @@ int ret; int CodecOggOpus::encoderInit(char* filePath, int sampleRate) { + // Create default, empty comment header comments = ope_comments_create(); - int numChannels = 1; // Mono audio - int family = 0; // Channel Mapping Family 0, used for mono/stereo streams + // Mono audio + int numChannels = 1; + // Channel Mapping Family 0, used for mono/stereo streams + int family = 0; + // Create encoder to encode PCM chunks and write the result to a file with the OggOpus framing encoder = ope_encoder_create_file(filePath, comments, sampleRate, numChannels, family, &ret); if (ret != OPE_OK) { LOGE(TAG, "Creation of OggOpusEnc failed."); @@ -41,12 +45,16 @@ int CodecOggOpus::setBitrate(int bitrate) { } int CodecOggOpus::writeFrame(short* frame, int samplesPerChannel) { + // Encode the raw PCM-16 buffer to Opus and write it to the ogg file return ope_encoder_write(encoder, frame, samplesPerChannel); } void CodecOggOpus::encoderRelease() { + // Finish any pending encode/write operations ope_encoder_drain(encoder); + // De-init the encoder instance ope_encoder_destroy(encoder); + // De-init the comment header struct ope_comments_destroy(comments); } diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h index 5c94a0d874..f1035434c5 100644 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h +++ b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h @@ -18,7 +18,16 @@ #define ELEMENT_ANDROID_CODECOGGOPUS_H #include - +/** + * This class is a wrapper around libopusenc, used to encode and write Opus frames into an Ogg file. + * + * The usual flow would be: + * + * 1. Use encoderInit to initialize the internal encoder with the sample rate and the path to write the encoded frames to. + * 2. (Optional) set the bitrate to use. + * 3. While recording, read PCM-16 chunks from the recorder, feed them to the encoder using writeFrame. + * 4. When finished, call encoderRelease to free some resources. + */ class CodecOggOpus { private: diff --git a/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt b/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt index 8f035e1660..8af11f8516 100644 --- a/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt +++ b/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt @@ -20,6 +20,9 @@ import android.util.Log import androidx.annotation.IntRange import im.vector.opusencoder.configuration.SampleRate +/** + * JNI bridge to CodecOggOpus in the native opuscodec library. + */ class OggOpusEncoder { companion object { From 2fbec133b655c018733c82d4f508c93cae05f539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 7 Jun 2022 17:14:22 +0200 Subject: [PATCH 098/314] Address review comments, fix quality check issues. --- .../im/vector/opusencoder/configuration/SampleRate.kt | 10 +++++----- .../im/vector/app/features/voice/VoiceRecorderL.kt | 7 +------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt b/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt index 70f8590863..e1a8f10725 100644 --- a/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt +++ b/library/opusencoder/src/main/java/im/vector/opusencoder/configuration/SampleRate.kt @@ -20,9 +20,9 @@ package im.vector.opusencoder.configuration * Sampling rate of the input signal in Hz. */ sealed class SampleRate private constructor(val value: Int) { - object _8khz : SampleRate(8000) - object _12kHz : SampleRate(12000) - object _16kHz : SampleRate(16000) - object _24KHz : SampleRate(24000) - object _48kHz : SampleRate(48000) + object Rate8khz : SampleRate(8000) + object Rate12kHz : SampleRate(12000) + object Rate16kHz : SampleRate(16000) + object Rate24KHz : SampleRate(24000) + object Rate48kHz : SampleRate(48000) } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index 8816ffafbd..f0eed41637 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -27,7 +27,6 @@ import im.vector.opusencoder.OggOpusEncoder import im.vector.opusencoder.configuration.SampleRate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -46,7 +45,7 @@ class VoiceRecorderL( ) : VoiceRecorder { companion object { - private val SAMPLE_RATE = SampleRate._48kHz + private val SAMPLE_RATE = SampleRate.Rate48kHz private const val BITRATE = 24 * 1024 } @@ -101,10 +100,6 @@ class VoiceRecorderL( initializeCodec(outputFile.absolutePath) recordingJob = recorderScope.launch { - while (audioRecorder?.state != AudioRecord.STATE_INITIALIZED) { - // If the recorder is not ready let's give it some extra time - delay(10L) - } audioRecorder?.startRecording() val buffer = ShortArray(bufferSizeInShorts) From 64334c343719c0849ce70fd4e0fa99ed860e1836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 10 Jun 2022 09:51:20 +0200 Subject: [PATCH 099/314] Add some tests --- dependencies.gradle | 2 +- vector/build.gradle | 1 + .../vector/app/AndroidVersionTestOverrider.kt | 46 +++++++++++ .../app/features/voice/VoiceRecorderLTests.kt | 79 +++++++++++++++++++ .../voice/VoiceRecorderProviderTests.kt | 47 +++++++++++ .../app/features/voice/VoiceRecorderQTests.kt | 78 ++++++++++++++++++ .../app/features/voice/VoiceRecorderTests.kt | 56 +++++++++++++ .../features/voice/AbstractVoiceRecorder.kt | 2 +- 8 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt create mode 100644 vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt create mode 100644 vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt create mode 100644 vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt create mode 100644 vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt diff --git a/dependencies.gradle b/dependencies.gradle index 272a26886b..fdc2c5d941 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -29,7 +29,7 @@ def jjwt = "0.11.5" def vanniktechEmoji = "0.15.0" // Testing -def mockk = "1.12.4" +def mockk = "1.12.3" def espresso = "3.4.0" def androidxTest = "1.4.0" def androidxOrchestrator = "1.4.1" diff --git a/vector/build.gradle b/vector/build.gradle index 7aa7ba7366..256ffd5b1f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -547,5 +547,6 @@ dependencies { androidTestImplementation('com.adevinta.android:barista:4.2.0') { exclude group: 'org.jetbrains.kotlin' } + androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator } diff --git a/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt b/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt new file mode 100644 index 0000000000..97333b7c98 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import android.os.Build +import java.lang.reflect.Field + +/** + * Used to override [Build.VERSION.SDK_INT]. Ideally an interface should be used instead, but that approach forces us to either add suppress lint annotations + * and potentially miss an API version issue or write a custom lint rule, which seems like an overkill. + */ +object AndroidVersionTestOverrider { + + private var initialValue: Int? = null + + fun override(newVersion: Int) { + if (initialValue == null) { + initialValue = Build.VERSION.SDK_INT + } + val field = Build.VERSION::class.java.getField("SDK_INT") + setStaticField(field, newVersion) + } + + fun restore() { + initialValue?.let { override(it) } + } + + private fun setStaticField(field: Field, value: Any) { + field.isAccessible = true + field.set(null, value) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt new file mode 100644 index 0000000000..a7cc252fb7 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt @@ -0,0 +1,79 @@ +/* + * 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.voice + +import android.Manifest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldExist +import org.amshove.kluent.shouldNotBeNull +import org.amshove.kluent.shouldNotExist +import org.junit.Rule +import org.junit.Test +import java.io.File + +class VoiceRecorderLTests { + + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val recorder = spyk(VoiceRecorderL(context, Dispatchers.IO)) + + @Test + fun startRecordCreatesOggFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() + + startRecord("some_room_id") + + getVoiceMessageFile().shouldNotBeNullAndExist() + + stopRecord() + } + + @Test + fun stopRecordKeepsFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() + + startRecord("some_room_id") + stopRecord() + + getVoiceMessageFile().shouldNotBeNullAndExist() + } + + @Test + fun cancelRecordRemovesFileAfterStopping() = with(recorder) { + startRecord("some_room_id") + val file = recorder.getVoiceMessageFile() + file.shouldNotBeNullAndExist() + + cancelRecord() + + verify { stopRecord() } + getVoiceMessageFile().shouldBeNull() + file!!.shouldNotExist() + } +} + +private fun File?.shouldNotBeNullAndExist() { + shouldNotBeNull() + shouldExist() +} diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt new file mode 100644 index 0000000000..c7105b613f --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt @@ -0,0 +1,47 @@ +/* + * 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.voice + +import android.os.Build +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.AndroidVersionTestOverrider +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.After +import org.junit.Test + +class VoiceRecorderProviderTests { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val provider = VoiceRecorderProvider(context) + + @After + fun tearDown() { + AndroidVersionTestOverrider.restore() + } + + @Test + fun provideVoiceRecorderOnAndroidQReturnsQRecorder() { + AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) + provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class) + } + + @Test + fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() { + AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP) + provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt new file mode 100644 index 0000000000..395c1f21d9 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt @@ -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.voice + +import android.Manifest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import io.mockk.spyk +import io.mockk.verify +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldExist +import org.amshove.kluent.shouldNotBeNull +import org.amshove.kluent.shouldNotExist +import org.junit.Rule +import org.junit.Test +import java.io.File + +class VoiceRecorderQTests { + + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val recorder = spyk(VoiceRecorderQ(context)) + + @Test + fun startRecordCreatesOggFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() + + startRecord("some_room_id") + + getVoiceMessageFile().shouldNotBeNullAndExist() + + stopRecord() + } + + @Test + fun stopRecordKeepsFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() + + startRecord("some_room_id") + stopRecord() + + getVoiceMessageFile().shouldNotBeNullAndExist() + } + + @Test + fun cancelRecordRemovesFileAfterStopping() = with(recorder) { + startRecord("some_room_id") + val file = recorder.getVoiceMessageFile() + file.shouldNotBeNullAndExist() + + cancelRecord() + + verify { stopRecord() } + getVoiceMessageFile().shouldBeNull() + file!!.shouldNotExist() + } +} + +private fun File?.shouldNotBeNullAndExist() { + shouldNotBeNull() + shouldExist() +} diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt new file mode 100644 index 0000000000..7feeff83cb --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt @@ -0,0 +1,56 @@ +/* + * 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.voice + +import android.net.Uri +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.Dispatchers +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldExist +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import java.io.File + +class VoiceRecorderTests { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val voiceRecorder = VoiceRecorderL(context, Dispatchers.IO) + private val audioDirectory = File(context.cacheDir, "voice_records") + + @After + fun tearDown() { + audioDirectory.deleteRecursively() + } + + @Test + fun ensureAudioDirectoryCreatesIt() { + voiceRecorder.ensureAudioDirectory(context) + audioDirectory.shouldExist() + } + + @Test + fun findVoiceFileSearchesInDirectory() { + val filename = "someFile.ogg" + val attachment = ContentAttachmentData( + queryUri = Uri.parse(filename), + mimeType = "ogg", + type = ContentAttachmentData.Type.AUDIO + ) + attachment.findVoiceFile(audioDirectory) shouldBeEqualTo File(audioDirectory, filename) + } +} diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt index 13b8149f83..91eb371f42 100644 --- a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -27,7 +27,7 @@ import java.util.UUID abstract class AbstractVoiceRecorder( private val context: Context, - internal val filenameExt: String + private val filenameExt: String, ) : VoiceRecorder { private val outputDirectory: File by lazy { ensureAudioDirectory(context) } From c204f41becfa5c893a5d11c94ffe343fea9ebb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 15 Jun 2022 11:34:18 +0200 Subject: [PATCH 100/314] Try to fix tests, address review comments. --- dependencies.gradle | 2 +- vector/build.gradle | 1 - .../app/features/voice/VoiceRecorderLTests.kt | 4 +- .../app/features/voice/VoiceRecorderQTests.kt | 53 ++++++++++++------- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index fdc2c5d941..604174fe57 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -29,7 +29,7 @@ def jjwt = "0.11.5" def vanniktechEmoji = "0.15.0" // Testing -def mockk = "1.12.3" +def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" def androidxTest = "1.4.0" def androidxOrchestrator = "1.4.1" diff --git a/vector/build.gradle b/vector/build.gradle index 256ffd5b1f..46659f66a8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -337,7 +337,6 @@ android { } dependencies { - implementation fileTree(dir: "libs", include: ["*.aar", "*.jar"]) implementation project(':library:opusencoder') implementation project(":vector-config") diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt index a7cc252fb7..c02c2cac80 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt @@ -20,7 +20,6 @@ import android.Manifest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.mockk.spyk -import io.mockk.verify import kotlinx.coroutines.Dispatchers import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldExist @@ -60,14 +59,13 @@ class VoiceRecorderLTests { } @Test - fun cancelRecordRemovesFileAfterStopping() = with(recorder) { + fun cancelRecordRemovesFile() = with(recorder) { startRecord("some_room_id") val file = recorder.getVoiceMessageFile() file.shouldNotBeNullAndExist() cancelRecord() - verify { stopRecord() } getVoiceMessageFile().shouldBeNull() file!!.shouldNotExist() } diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt index 395c1f21d9..446d9e5b21 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt @@ -17,10 +17,14 @@ package im.vector.app.features.voice import android.Manifest +import android.os.Build +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldExist import org.amshove.kluent.shouldNotBeNull @@ -29,6 +33,7 @@ import org.junit.Rule import org.junit.Test import java.io.File +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) class VoiceRecorderQTests { @get:Rule @@ -38,38 +43,50 @@ class VoiceRecorderQTests { private val recorder = spyk(VoiceRecorderQ(context)) @Test - fun startRecordCreatesOggFile() = with(recorder) { - getVoiceMessageFile().shouldBeNull() + fun startRecordCreatesOggFile() = runBlocking { + with(recorder) { + getVoiceMessageFile().shouldBeNull() - startRecord("some_room_id") + startRecord("some_room_id") + waitForRecording() - getVoiceMessageFile().shouldNotBeNullAndExist() + getVoiceMessageFile().shouldNotBeNullAndExist() - stopRecord() + stopRecord() + } } @Test - fun stopRecordKeepsFile() = with(recorder) { - getVoiceMessageFile().shouldBeNull() + fun stopRecordKeepsFile() = runBlocking { + with(recorder) { + getVoiceMessageFile().shouldBeNull() - startRecord("some_room_id") - stopRecord() + startRecord("some_room_id") + waitForRecording() + stopRecord() - getVoiceMessageFile().shouldNotBeNullAndExist() + getVoiceMessageFile().shouldNotBeNullAndExist() + } } @Test - fun cancelRecordRemovesFileAfterStopping() = with(recorder) { - startRecord("some_room_id") - val file = recorder.getVoiceMessageFile() - file.shouldNotBeNullAndExist() + fun cancelRecordRemovesFileAfterStopping() = runBlocking { + with(recorder) { + startRecord("some_room_id") + val file = recorder.getVoiceMessageFile() + file.shouldNotBeNullAndExist() - cancelRecord() + waitForRecording() + cancelRecord() - verify { stopRecord() } - getVoiceMessageFile().shouldBeNull() - file!!.shouldNotExist() + verify { stopRecord() } + getVoiceMessageFile().shouldBeNull() + file!!.shouldNotExist() + } } + + // Give MediaRecorder some time to actually start recording + private suspend fun waitForRecording() = delay(10) } private fun File?.shouldNotBeNullAndExist() { From 0ed9a1885c06b6621c5ba36e487dbeb339b51ae4 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 15 Jun 2022 15:55:02 +0300 Subject: [PATCH 101/314] Test poll view state with a question and max number of options. --- .../poll/create/CreatePollViewModelTest.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index ee6fb8db18..017ed0a31c 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -17,7 +17,6 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.test.MvRxTestRule -import im.vector.app.features.login.SignMode import im.vector.app.features.poll.PollMode import im.vector.app.features.poll.create.CreatePollViewStates.createPollArgs import im.vector.app.features.poll.create.CreatePollViewStates.editPollArgs @@ -26,10 +25,12 @@ import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions +import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndMaxOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test +import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -63,6 +64,8 @@ class CreatePollViewModelTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + // We need to wait for createPollViewModel.onChange is triggered + delay(10) createPollViewModel .test() .assertState(pollViewStateWithOnlyQuestion) @@ -77,6 +80,7 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) } + delay(10) createPollViewModel .test() .assertState(pollViewStateWithQuestionAndNotEnoughOptions) @@ -90,6 +94,7 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) } + delay(10) createPollViewModel .test() .assertState(pollViewStateWithoutQuestionAndEnoughOptions) @@ -104,9 +109,28 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) } + delay(10) createPollViewModel .test() .assertState(pollViewStateWithQuestionAndEnoughOptions) .finish() } + + @Test + fun `given there is a question when max number of options are added then poll can be created and more options cannot be added`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + repeat(CreatePollViewModel.MAX_OPTIONS_COUNT) { + if (it >= CreatePollViewModel.MIN_OPTIONS_COUNT) { + createPollViewModel.handle(CreatePollAction.OnAddOption) + } + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + } + + delay(10) + createPollViewModel + .test() + .assertState(pollViewStateWithQuestionAndMaxOptions) + .finish() + } } From e12103387de8ae67d04ebf97df544eaa9338a98d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 11:54:14 +0200 Subject: [PATCH 102/314] Setup Flipper Move getLastSession() to the SessionManager Create `DebugService` Move `logDbUsageInfo()` to `DebugService` --- changelog.d/6300.misc | 1 + dependencies_groups.gradle | 4 ++ docs/flipper.md | 7 ++ .../android/sdk/common/TestMatrixComponent.kt | 2 + .../java/org/matrix/android/sdk/api/Matrix.kt | 7 ++ .../android/sdk/api/MatrixConfiguration.kt | 5 ++ .../android/sdk/api/debug/DebugService.kt | 34 +++++++++ .../matrix/android/sdk/api/session/Session.kt | 8 ++- .../sdk/api/session/crypto/CryptoService.kt | 2 - .../android/sdk/internal/SessionManager.kt | 7 ++ .../auth/DefaultAuthenticationService.kt | 5 +- .../internal/crypto/DefaultCryptoService.kt | 4 -- .../internal/crypto/store/IMXCryptoStore.kt | 1 - .../crypto/store/db/RealmCryptoStore.kt | 8 --- .../android/sdk/internal/debug/DebugModule.kt | 29 ++++++++ .../sdk/internal/debug/DefaultDebugService.kt | 44 ++++++++++++ .../sdk/internal/di/MatrixComponent.kt | 2 + .../android/sdk/internal/di/NetworkModule.kt | 3 + .../sdk/internal/session/DefaultSession.kt | 18 +++++ vector/build.gradle | 10 +++ vector/src/debug/AndroidManifest.xml | 4 ++ .../im/vector/app/flipper/FlipperProxy.kt | 72 +++++++++++++++++++ .../java/im/vector/app/VectorApplication.kt | 3 + .../im/vector/app/core/di/SingletonModule.kt | 7 +- .../app/features/rageshake/BugReporter.kt | 6 +- .../im/vector/app/flipper/FlipperProxy.kt | 31 ++++++++ 26 files changed, 298 insertions(+), 26 deletions(-) create mode 100644 changelog.d/6300.misc create mode 100644 docs/flipper.md create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt create mode 100644 vector/src/debug/java/im/vector/app/flipper/FlipperProxy.kt create mode 100644 vector/src/release/java/im/vector/app/flipper/FlipperProxy.kt diff --git a/changelog.d/6300.misc b/changelog.d/6300.misc new file mode 100644 index 0000000000..0ac762e595 --- /dev/null +++ b/changelog.d/6300.misc @@ -0,0 +1 @@ +Setup [Flipper](https://fbflipper.com/) diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 59cefe7e89..47d53c6ed9 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -31,6 +31,7 @@ ext.groups = [ ], group: [ 'com.android', + 'com.android.ndk.thirdparty', 'com.android.tools', 'com.google.firebase', 'com.google.testing.platform', @@ -52,6 +53,7 @@ ext.groups = [ 'com.dropbox.core', 'com.soywiz.korlibs.korte', 'com.facebook.fbjni', + 'com.facebook.flipper', 'com.facebook.fresco', 'com.facebook.infer.annotation', 'com.facebook.soloader', @@ -93,6 +95,7 @@ ext.groups = [ 'com.ibm.icu', 'com.jakewharton.android.repackaged', 'com.jakewharton.timber', + 'com.kgurgul.flipper', 'com.linkedin.dexmaker', 'com.mapbox.mapboxsdk', 'com.nulab-inc', @@ -168,6 +171,7 @@ ext.groups = [ 'org.glassfish.jaxb', 'org.hamcrest', 'org.jacoco', + 'org.java-websocket', 'org.jetbrains', 'org.jetbrains.dokka', 'org.jetbrains.intellij.deps', diff --git a/docs/flipper.md b/docs/flipper.md new file mode 100644 index 0000000000..490a2b5322 --- /dev/null +++ b/docs/flipper.md @@ -0,0 +1,7 @@ +# Flipper + +TODO Write doc +- Setup env +- Debug activity +>adb shell am start -n im.vector.app.debug/com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity + diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt index daf6b73313..52beb1b484 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -21,6 +21,7 @@ import dagger.BindsInstance import dagger.Component import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.auth.AuthModule +import org.matrix.android.sdk.internal.debug.DebugModule import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.di.MatrixModule import org.matrix.android.sdk.internal.di.MatrixScope @@ -36,6 +37,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule NetworkModule::class, AuthModule::class, RawModule::class, + DebugModule::class, SettingsModule::class, SystemModule::class ] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 979201706b..55569580a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -25,6 +25,7 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.debug.DebugService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.network.ApiInterceptorListener import org.matrix.android.sdk.api.network.ApiPath @@ -54,6 +55,7 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { @Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var rawService: RawService + @Inject internal lateinit var debugService: DebugService @Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @@ -93,6 +95,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { */ fun rawService() = rawService + /** + * Return the DebugService. + */ + fun debugService() = debugService + /** * Return the LightweightSettingsStorage. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 21106fba6c..893e90fb3e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api import okhttp3.ConnectionSpec +import okhttp3.Interceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig import java.net.Proxy @@ -65,4 +66,8 @@ data class MatrixConfiguration( * Thread messages default enable/disabled value. */ val threadMessagesEnabledDefault: Boolean = false, + /** + * List of network interceptors, they will be added when building an OkHttp client. + */ + val networkInterceptors: List = emptyList(), ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt new file mode 100644 index 0000000000..d0cee08831 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt @@ -0,0 +1,34 @@ +/* + * 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.debug + +import io.realm.RealmConfiguration + +/** + * Useful methods to access to some private data managed by the SDK. + */ +interface DebugService { + /** + * Get all the available Realm Configuration. + */ + fun getAllRealmConfigurations(): List + + /** + * Prints out info on DB size to logcat. + */ + fun logDbUsageInfo() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index b3a629094c..a0d122635d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session import androidx.annotation.MainThread +import io.realm.RealmConfiguration import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams @@ -334,7 +335,12 @@ interface Session { fun getUiaSsoFallbackUrl(authenticationSessionId: String): String /** - * Maintenance API, allows to print outs info on DB size to logcat. + * Debug API, will print out info on DB size to logcat. */ fun logDbUsageInfo() + + /** + * Debug API, return the list of all RealmConfiguration used by this session. + */ + fun getRealmConfigurations(): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 9cc87b6f71..638da11804 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -171,8 +171,6 @@ interface CryptoService { fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? - fun logDbUsageInfo() - /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt index bd2dac9e3c..5f5bb1f951 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt @@ -40,6 +40,13 @@ internal class SessionManager @Inject constructor( return getOrCreateSessionComponent(sessionParams) } + fun getLastSession(): Session? { + val sessionParams = sessionParamsStore.getLast() + return sessionParams?.let { + getOrCreateSession(it) + } + } + fun getOrCreateSession(sessionParams: SessionParams): Session { return getOrCreateSessionComponent(sessionParams).session() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 61a423669c..92852e4722 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -73,10 +73,7 @@ internal class DefaultAuthenticationService @Inject constructor( } override fun getLastAuthenticatedSession(): Session? { - val sessionParams = sessionParamsStore.getLast() - return sessionParams?.let { - sessionManager.getOrCreateSession(it) - } + return sessionManager.getLastSession() } override suspend fun getLoginFlowOfSession(sessionId: String): LoginFlowResult { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 719f366518..e0bcde2296 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -1298,10 +1298,6 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) } - override fun logDbUsageInfo() { - cryptoStore.logDbUsageInfo() - } - override fun prepareToEncrypt(roomId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index b18de34329..b5b8d8e974 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -513,6 +513,5 @@ internal interface IMXCryptoStore { fun setDeviceKeysUploaded(uploaded: Boolean) fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() - fun logDbUsageInfo() fun getOutgoingRoomKeyRequests(inStates: Set): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index c56e4d320b..028d8f73f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -88,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider @@ -1709,11 +1708,4 @@ internal class RealmCryptoStore @Inject constructor( // Can we do something for WithHeldSessionEntity? } } - - /** - * Prints out database info. - */ - override fun logDbUsageInfo() { - RealmDebugTools(realmConfiguration).logInfo("Crypto") - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt new file mode 100644 index 0000000000..f392d39d7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt @@ -0,0 +1,29 @@ +/* + * 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.internal.debug + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.api.debug.DebugService + +@Module +internal abstract class DebugModule { + + @Binds + abstract fun bindDebugService(service: DefaultDebugService): DebugService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt new file mode 100644 index 0000000000..3f2e6fafc8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt @@ -0,0 +1,44 @@ +/* + * 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.internal.debug + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.debug.DebugService +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.database.tools.RealmDebugTools +import org.matrix.android.sdk.internal.di.AuthDatabase +import org.matrix.android.sdk.internal.di.GlobalDatabase +import javax.inject.Inject + +internal class DefaultDebugService @Inject constructor( + @AuthDatabase private val realmConfigurationAuth: RealmConfiguration, + @GlobalDatabase private val realmConfigurationGlobal: RealmConfiguration, + private val sessionManager: SessionManager, +) : DebugService { + + override fun getAllRealmConfigurations(): List { + return sessionManager.getLastSession()?.getRealmConfigurations().orEmpty() + + realmConfigurationAuth + + realmConfigurationGlobal + } + + override fun logDbUsageInfo() { + RealmDebugTools(realmConfigurationAuth).logInfo("Auth") + RealmDebugTools(realmConfigurationGlobal).logInfo("Global") + sessionManager.getLastSession()?.logDbUsageInfo() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index 095916643c..d668c0498f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.AuthModule import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.debug.DebugModule import org.matrix.android.sdk.internal.raw.RawModule import org.matrix.android.sdk.internal.session.MockHttpInterceptor import org.matrix.android.sdk.internal.session.TestInterceptor @@ -49,6 +50,7 @@ import java.io.File NetworkModule::class, AuthModule::class, RawModule::class, + DebugModule::class, SettingsModule::class, SystemModule::class, NoOpTestModule::class diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index 862cf463b2..4d0708bdb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -95,6 +95,9 @@ internal object NetworkModule { matrixConfiguration.proxy?.let { proxy(it) } + matrixConfiguration.networkInterceptors.forEach { + addInterceptor(it) + } } .connectionSpecs(Collections.singletonList(spec)) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 32269c9afd..36d3f0606b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -71,6 +71,9 @@ import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.tools.RealmDebugTools +import org.matrix.android.sdk.internal.di.ContentScannerDatabase +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.IdentityDatabase import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate @@ -88,6 +91,9 @@ internal class DefaultSession @Inject constructor( override val sessionId: String, override val coroutineDispatchers: MatrixCoroutineDispatchers, @SessionDatabase private val realmConfiguration: RealmConfiguration, + @CryptoDatabase private val realmConfigurationCrypto: RealmConfiguration, + @IdentityDatabase private val realmConfigurationIdentity: RealmConfiguration, + @ContentScannerDatabase private val realmConfigurationContentScanner: RealmConfiguration, private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>, private val sessionListeners: SessionListeners, private val roomService: Lazy, @@ -265,5 +271,17 @@ internal class DefaultSession @Inject constructor( override fun logDbUsageInfo() { RealmDebugTools(realmConfiguration).logInfo("Session") + RealmDebugTools(realmConfigurationCrypto).logInfo("Crypto") + RealmDebugTools(realmConfigurationIdentity).logInfo("Identity") + RealmDebugTools(realmConfigurationContentScanner).logInfo("ContentScanner") + } + + override fun getRealmConfigurations(): List { + return listOf( + realmConfiguration, + realmConfigurationCrypto, + realmConfigurationIdentity, + realmConfigurationContentScanner, + ) } } diff --git a/vector/build.gradle b/vector/build.gradle index 46659f66a8..7b73acda42 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -525,6 +525,16 @@ dependencies { exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" } + // Flipper, debug builds only + debugImplementation('com.facebook.flipper:flipper:0.149.0') { + exclude group: 'com.facebook.fbjni', module: 'fbjni' + } + debugImplementation('com.facebook.flipper:flipper-network-plugin:0.149.0') { + exclude group: 'com.facebook.fbjni', module: 'fbjni' + } + debugImplementation 'com.facebook.soloader:soloader:0.10.3' + debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.1.0" + // Activate when you want to check for leaks, from time to time. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index 0b2b5cf90f..4ef1f41130 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -9,6 +9,10 @@ + + diff --git a/vector/src/debug/java/im/vector/app/flipper/FlipperProxy.kt b/vector/src/debug/java/im/vector/app/flipper/FlipperProxy.kt new file mode 100644 index 0000000000..76be7e1b46 --- /dev/null +++ b/vector/src/debug/java/im/vector/app/flipper/FlipperProxy.kt @@ -0,0 +1,72 @@ +/* + * 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.flipper + +import android.content.Context +import com.facebook.flipper.android.AndroidFlipperClient +import com.facebook.flipper.android.utils.FlipperUtils +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin +import com.facebook.flipper.plugins.inspector.DescriptorMapping +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin +import com.facebook.soloader.SoLoader +import com.kgurgul.flipper.RealmDatabaseDriver +import com.kgurgul.flipper.RealmDatabaseProvider +import io.realm.RealmConfiguration +import okhttp3.Interceptor +import org.matrix.android.sdk.api.Matrix +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FlipperProxy @Inject constructor( + private val context: Context, +) { + private val networkFlipperPlugin = NetworkFlipperPlugin() + + fun init(matrix: Matrix) { + SoLoader.init(context, false) + + if (FlipperUtils.shouldEnableFlipper(context)) { + val client = AndroidFlipperClient.getInstance(context) + client.addPlugin(CrashReporterPlugin.getInstance()) + client.addPlugin(SharedPreferencesFlipperPlugin(context)) + client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) + client.addPlugin(networkFlipperPlugin) + client.addPlugin( + DatabasesFlipperPlugin( + RealmDatabaseDriver( + context = context, + realmDatabaseProvider = object : RealmDatabaseProvider { + override fun getRealmConfigurations(): List { + return matrix.debugService().getAllRealmConfigurations() + } + }) + ) + ) + client.start() + } + } + + @Suppress("RedundantNullableReturnType") + fun getNetworkInterceptor(): Interceptor? { + return FlipperOkhttpInterceptor(networkFlipperPlugin) + } +} diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 7db0f99f5f..05cb4fb9bc 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -60,6 +60,7 @@ import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.version.VersionProvider +import im.vector.app.flipper.FlipperProxy import im.vector.app.push.fcm.FcmHelper import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler import org.matrix.android.sdk.api.Matrix @@ -99,6 +100,7 @@ class VectorApplication : @Inject lateinit var autoRageShaker: AutoRageShaker @Inject lateinit var vectorFileLogger: VectorFileLogger @Inject lateinit var vectorAnalytics: VectorAnalytics + @Inject lateinit var flipperProxy: FlipperProxy @Inject lateinit var matrix: Matrix // font thread handler @@ -117,6 +119,7 @@ class VectorApplication : enableStrictModeIfNeeded() super.onCreate() appContext = this + flipperProxy.init(matrix) vectorAnalytics.init() invitesAcceptor.initialize() autoRageShaker.initialize() diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index 085a9a5d12..bc0bccfa1b 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -50,6 +50,7 @@ import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository +import im.vector.app.flipper.FlipperProxy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -119,12 +120,16 @@ object VectorStaticModule { @Provides fun providesMatrixConfiguration( vectorPreferences: VectorPreferences, - vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider + vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider, + flipperProxy: FlipperProxy, ): MatrixConfiguration { return MatrixConfiguration( applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider, threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(), + networkInterceptors = listOfNotNull( + flipperProxy.getNetworkInterceptor(), + ) ) } diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 2612e63841..09453e5b02 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -492,11 +492,7 @@ class BugReporter @Inject constructor( */ fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) { screenshot = takeScreenshot(activity) - activeSessionHolder.getSafeActiveSession()?.let { - it.logDbUsageInfo() - it.cryptoService().logDbUsageInfo() - } - + matrix.debugService().logDbUsageInfo() activity.startActivity(BugReportActivity.intent(activity, reportType)) } diff --git a/vector/src/release/java/im/vector/app/flipper/FlipperProxy.kt b/vector/src/release/java/im/vector/app/flipper/FlipperProxy.kt new file mode 100644 index 0000000000..03b1977a45 --- /dev/null +++ b/vector/src/release/java/im/vector/app/flipper/FlipperProxy.kt @@ -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.flipper + +import okhttp3.Interceptor +import org.matrix.android.sdk.api.Matrix +import javax.inject.Inject + +/** + * No op version. + */ +@Suppress("UNUSED_PARAMETER") +class FlipperProxy @Inject constructor() { + fun init(matrix: Matrix) {} + + fun getNetworkInterceptor(): Interceptor? = null +} From aea94d79eb9fd19e17656176a3b898592d7d39bd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Jun 2022 12:43:21 +0200 Subject: [PATCH 103/314] Add documentation for Flipper --- docs/flipper.md | 58 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/docs/flipper.md b/docs/flipper.md index 490a2b5322..cc3b7eb5e0 100644 --- a/docs/flipper.md +++ b/docs/flipper.md @@ -1,7 +1,57 @@ # Flipper -TODO Write doc -- Setup env -- Debug activity ->adb shell am start -n im.vector.app.debug/com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity + +* [Introduction](#introduction) +* [Setup](#setup) + * [Troubleshoot](#troubleshoot) + * [No device found issue](#no-device-found-issue) + * [Diagnostic Activity](#diagnostic-activity) + * [Other](#other) +* [Links](#links) + + + +## Introduction + +[Flipper](https://fbflipper.com) is a powerful tool from Meta, which allow to inspect the running application details and states from your computer. + +Flipper is configured in the Element Android project to let the developers be able to: +- inspect all the Realm databases content; +- do layout inspection; +- see the crash logs; +- see the logcat; +- see all the network requests; +- see all the SharedPreferences; +- take screenshots and record videos of the device; +- and more! + +## Setup + +- Install Flipper on your computer. Follow instructions here: https://fbflipper.com/docs/getting-started/index/ +- Run the debug version of Element on an emulator or on a real device. + +### Troubleshoot + +#### No device found issue + +The configuration of the Flipper application has to be updated. The issue has been asked and answered here: https://stackoverflow.com/questions/71744103/android-emulator-unable-to-connect-to-flipper/72608113#72608113 + +#### Diagnostic Activity + +Flipper comes with a Diagnostic Activity that you can start from command line using: + +```shell +adb shell am start -n im.vector.app.debug/com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity +``` + +It provides some log which can help to figure out what's going on client side. + +#### Other + +https://fbflipper.com/docs/getting-started/troubleshooting/android/ may help. + +## Links + +- https://fbflipper.com +- Realm Plugin for Flipper: https://github.com/kamgurgul/Flipper-Realm From 514c4234f2edbf8cb582692b83a175479632d5cf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Jun 2022 10:30:38 +0200 Subject: [PATCH 104/314] Add MatrixConfiguration network interceptors after all the other, to ensure to view all the network request headers in Flipper. --- .../android/sdk/internal/di/NetworkModule.kt | 8 ++------ .../network/httpclient/OkHttpClientUtil.kt | 15 +++++++++++++++ .../sdk/internal/session/SessionModule.kt | 17 ++++++++++++----- .../internal/session/identity/IdentityModule.kt | 6 +++++- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index 4d0708bdb3..b5b46a3f5a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.network.ApiInterceptor import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.UserAgentInterceptor +import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger import java.util.Collections @@ -92,14 +93,9 @@ internal object NetworkModule { if (BuildConfig.LOG_PRIVATE_DATA) { addInterceptor(curlLoggingInterceptor) } - matrixConfiguration.proxy?.let { - proxy(it) - } - matrixConfiguration.networkInterceptors.forEach { - addInterceptor(it) - } } .connectionSpecs(Collections.singletonList(spec)) + .applyMatrixConfiguration(matrixConfiguration) .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt index 3920c3b527..1c395c2d61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.network.httpclient import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.internal.network.AccessTokenInterceptor import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor @@ -51,3 +52,17 @@ internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: H return this } + +internal fun OkHttpClient.Builder.applyMatrixConfiguration(matrixConfiguration: MatrixConfiguration): OkHttpClient.Builder { + matrixConfiguration.proxy?.let { + proxy(it) + } + + // Move networkInterceptors provided in the configuration after all the others + interceptors().removeAll(matrixConfiguration.networkInterceptors) + matrixConfiguration.networkInterceptors.forEach { + addInterceptor(it) + } + + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 2c2317de0d..2cb9768231 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -73,6 +73,7 @@ import org.matrix.android.sdk.internal.network.PreferredNetworkCallbackStrategy import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider @@ -212,7 +213,7 @@ internal abstract class SessionModule { @UnauthenticatedWithCertificate fun providesOkHttpClientWithCertificate( @Unauthenticated okHttpClient: OkHttpClient, - homeServerConnectionConfig: HomeServerConnectionConfig + homeServerConnectionConfig: HomeServerConnectionConfig, ): OkHttpClient { return okHttpClient .newBuilder() @@ -228,7 +229,8 @@ internal abstract class SessionModule { @UnauthenticatedWithCertificate okHttpClient: OkHttpClient, @Authenticated accessTokenProvider: AccessTokenProvider, @SessionId sessionId: String, - @MockHttpInterceptor testInterceptor: TestInterceptor? + @MockHttpInterceptor testInterceptor: TestInterceptor?, + matrixConfiguration: MatrixConfiguration, ): OkHttpClient { return okHttpClient .newBuilder() @@ -239,6 +241,7 @@ internal abstract class SessionModule { addInterceptor(testInterceptor) } } + .applyMatrixConfiguration(matrixConfiguration) .build() } @@ -248,9 +251,11 @@ internal abstract class SessionModule { @UnauthenticatedWithCertificateWithProgress fun providesProgressOkHttpClient( @UnauthenticatedWithCertificate okHttpClient: OkHttpClient, - downloadProgressInterceptor: DownloadProgressInterceptor + downloadProgressInterceptor: DownloadProgressInterceptor, + matrixConfiguration: MatrixConfiguration, ): OkHttpClient { - return okHttpClient.newBuilder() + return okHttpClient + .newBuilder() .apply { // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor val existingCurlInterceptors = interceptors().filterIsInstance() @@ -262,7 +267,9 @@ internal abstract class SessionModule { existingCurlInterceptors.forEach { addInterceptor(it) } - }.build() + } + .applyMatrixConfiguration(matrixConfiguration) + .build() } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt index 464ae96e3a..33d8164895 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -21,6 +21,7 @@ import dagger.Module import dagger.Provides import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.AuthenticatedIdentity @@ -29,6 +30,7 @@ import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.UserMd5 import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor +import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.session.SessionModule import org.matrix.android.sdk.internal.session.SessionScope @@ -49,11 +51,13 @@ internal abstract class IdentityModule { @AuthenticatedIdentity fun providesOkHttpClient( @UnauthenticatedWithCertificate okHttpClient: OkHttpClient, - @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider, + matrixConfiguration: MatrixConfiguration, ): OkHttpClient { return okHttpClient .newBuilder() .addAccessTokenInterceptor(accessTokenProvider) + .applyMatrixConfiguration(matrixConfiguration) .build() } From 0abeb3306ecc4ae039f62da5272a4e8d9c5b7844 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Jun 2022 10:37:17 +0200 Subject: [PATCH 105/314] Bad copy paste in comment --- .../org/matrix/android/sdk/internal/session/SessionModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 2cb9768231..950cb899f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -257,7 +257,7 @@ internal abstract class SessionModule { return okHttpClient .newBuilder() .apply { - // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + // Remove the previous CurlLoggingInterceptor, to add it after the downloadProgressInterceptor val existingCurlInterceptors = interceptors().filterIsInstance() interceptors().removeAll(existingCurlInterceptors) From 24d59eba87b0cd5397dbe5864d11d5e7679a41bb Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 15 Jun 2022 17:51:13 +0300 Subject: [PATCH 106/314] Test poll view events when create poll is requested. --- .../poll/create/CreatePollViewModelTest.kt | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 017ed0a31c..fa24d9c4a6 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -22,25 +22,38 @@ import im.vector.app.features.poll.create.CreatePollViewStates.createPollArgs import im.vector.app.features.poll.create.CreatePollViewStates.editPollArgs import im.vector.app.features.poll.create.CreatePollViewStates.fakeOptions import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion +import im.vector.app.features.poll.create.CreatePollViewStates.fakeRoomId import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndMaxOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions -import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.util.Cancelable class CreatePollViewModelTest { @get:Rule val mvrxTestRule = MvRxTestRule() - private val fakeSession = FakeSession() + private val fakeSession = mockk() + private val fakeRoom = mockk() + private val fakeSendService = mockk() + private val fakeCancellable = mockk() private fun createPollViewModel(pollMode: PollMode): CreatePollViewModel { return if (pollMode == PollMode.EDIT) { @@ -50,6 +63,13 @@ class CreatePollViewModelTest { } } + @Before + fun setup() { + every { fakeSession.getRoom(fakeRoomId) } returns fakeRoom + every { fakeRoom.sendService() } returns fakeSendService + every { fakeSendService.sendPoll(any(), fakeQuestion, any()) } returns fakeCancellable + } + @Test fun `given the view model is initialized then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) @@ -133,4 +153,50 @@ class CreatePollViewModelTest { .assertState(pollViewStateWithQuestionAndMaxOptions) .finish() } + + @Test + fun `given an initial poll state when poll type is changed then view state is updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.DISCLOSED)) + createPollViewModel.awaitState().pollType shouldBe PollType.DISCLOSED + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.UNDISCLOSED)) + createPollViewModel.awaitState().pollType shouldBe PollType.UNDISCLOSED + } + + @Test + fun `given there is not a question and enough options when create poll is requested then error view events are post`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, fakeOptions[0])) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.EmptyQuestionError, + CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT), + CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT) + ) + } + + @Test + fun `given there is a question and enough options when create poll is requested then success view event is post`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, fakeOptions[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, fakeOptions[1])) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.Success + ) + } } From a4dd2793522a9060a9b03e278c65f825ed2a921c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Jun 2022 19:19:32 +0200 Subject: [PATCH 107/314] Ensure we do not use `QueryStringValue.NoCondition` or `QueryStringValue.IsNull` to query for State Event. Also remove default value for those parameters. --- .../java/org/matrix/android/sdk/flow/FlowRoom.kt | 10 +++++----- .../org/matrix/android/sdk/flow/FlowSession.kt | 4 ++-- .../android/sdk/api/query/QueryStringValue.kt | 15 ++++++++++----- .../sdk/api/session/room/RoomExtensions.kt | 7 +++++-- .../sdk/api/session/room/state/StateService.kt | 10 +++++----- .../session/room/state/StateServiceExtension.kt | 2 +- .../sdk/api/session/widgets/WidgetService.kt | 6 +++--- .../session/permalinks/ViaParameterFinder.kt | 2 +- .../session/pushers/DefaultConditionResolver.kt | 3 ++- .../room/EventRelationsAggregationProcessor.kt | 2 +- .../room/membership/leaving/LeaveRoomTask.kt | 2 +- .../session/room/state/DefaultStateService.kt | 9 +++++---- .../session/room/state/StateEventDataSource.kt | 14 ++++++++------ .../room/timeline/LiveRoomStateListener.kt | 2 +- .../room/version/DefaultRoomVersionService.kt | 2 +- .../internal/session/space/DefaultSpaceService.kt | 2 +- .../session/widgets/DefaultWidgetService.kt | 6 +++--- .../sdk/internal/session/widgets/WidgetManager.kt | 7 ++++--- .../app/features/devtools/RoomDevToolViewModel.kt | 3 ++- .../home/room/detail/TimelineViewModel.kt | 4 ++-- .../detail/composer/MessageComposerViewModel.kt | 2 +- .../timeline/factory/MergedHeaderItemFactory.kt | 2 +- .../features/powerlevel/PowerLevelsFlowFactory.kt | 2 +- .../features/roomprofile/RoomProfileViewModel.kt | 4 ++-- .../roomprofile/alias/RoomAliasViewModel.kt | 2 +- .../members/RoomMemberListViewModel.kt | 5 +++-- .../roomprofile/settings/RoomSettingsViewModel.kt | 8 ++++---- .../RoomJoinRuleChooseRestrictedViewModel.kt | 2 +- .../spaces/leave/SpaceLeaveAdvancedViewModel.kt | 3 ++- .../app/features/widgets/WidgetPostAPIHandler.kt | 8 ++++---- .../app/features/widgets/WidgetViewModel.kt | 2 +- 31 files changed, 84 insertions(+), 68 deletions(-) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 90546756b8..7ac81b2d86 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.flow import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getStateEvent @@ -67,17 +67,17 @@ class FlowRoom(private val room: Room) { } } - fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Flow> { + fun liveStateEvent(eventType: String, stateKey: QueryStateEventValue): Flow> { return room.stateService().getStateEventLive(eventType, stateKey).asFlow() .startWith(room.coroutineDispatchers.io) { room.getStateEvent(eventType, stateKey).toOptional() } } - fun liveStateEvents(eventTypes: Set): Flow> { - return room.stateService().getStateEventsLive(eventTypes).asFlow() + fun liveStateEvents(eventTypes: Set, stateKey: QueryStateEventValue): Flow> { + return room.stateService().getStateEventsLive(eventTypes, stateKey).asFlow() .startWith(room.coroutineDispatchers.io) { - room.stateService().getStateEvents(eventTypes) + room.stateService().getStateEvents(eventTypes, stateKey) } } diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 1086928dc6..cc73e099b6 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.flow import androidx.lifecycle.asFlow import androidx.paging.PagedList import kotlinx.coroutines.flow.Flow -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo @@ -179,7 +179,7 @@ class FlowSession(private val session: Session) { fun liveRoomWidgets( roomId: String, - widgetId: QueryStringValue, + widgetId: QueryStateEventValue, widgetTypes: Set? = null, excludedTypes: Set? = null ): Flow> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt index f08c86885d..d3f6ec2287 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -16,6 +16,11 @@ package org.matrix.android.sdk.api.query +/** + * Only a subset of [QueryStringValue] are applicable to query the `stateKey` of a state event. + */ +sealed interface QueryStateEventValue + /** * Basic query language. All these cases are mutually exclusive. */ @@ -33,22 +38,22 @@ sealed interface QueryStringValue { /** * The tested field has to be not null. */ - object IsNotNull : QueryStringValue + object IsNotNull : QueryStringValue, QueryStateEventValue /** * The tested field has to be empty. */ - object IsEmpty : QueryStringValue + object IsEmpty : QueryStringValue, QueryStateEventValue /** - * The tested field has to not empty. + * The tested field has to be not empty. */ - object IsNotEmpty : QueryStringValue + object IsNotEmpty : QueryStringValue, QueryStateEventValue /** * Interface to check String content. */ - sealed interface ContentQueryStringValue : QueryStringValue { + sealed interface ContentQueryStringValue : QueryStringValue, QueryStateEventValue { val string: String val case: Case } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt index 0e631427bd..b30c60554f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt @@ -16,18 +16,21 @@ package org.matrix.android.sdk.api.session.room -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** * Get a TimelineEvent using the TimelineService of a Room. + * @param eventId The id of the event to retrieve */ fun Room.getTimelineEvent(eventId: String): TimelineEvent? = timelineService().getTimelineEvent(eventId) /** * Get a StateEvent using the StateService of a Room. + * @param eventType The type of the event, see [org.matrix.android.sdk.api.session.events.model.EventType]. + * @param stateKey the query which will be done on the stateKey. */ -fun Room.getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? = +fun Room.getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? = stateService().getStateEvent(eventType, stateKey) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index c79171f156..49c0debe1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.state import android.net.Uri import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -93,28 +93,28 @@ interface StateService { * @param eventType An eventType. * @param stateKey the query which will be done on the stateKey */ - fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? + fun getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? /** * Get a live state event of the room. * @param eventType An eventType. * @param stateKey the query which will be done on the stateKey */ - fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> + fun getStateEventLive(eventType: String, stateKey: QueryStateEventValue): LiveData> /** * Get state events of the room. * @param eventTypes Set of eventType. If empty, all state events will be returned * @param stateKey the query which will be done on the stateKey */ - fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): List + fun getStateEvents(eventTypes: Set, stateKey: QueryStateEventValue): List /** * Get live state events of the room. * @param eventTypes Set of eventType to observe. If empty, all state events will be observed * @param stateKey the query which will be done on the stateKey */ - fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> + fun getStateEventsLive(eventTypes: Set, stateKey: QueryStateEventValue): LiveData> suspend fun setJoinRulePublic() suspend fun setJoinRuleInviteOnly() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt index 9e45fc126d..6a9506fd9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent * Return true if a room can be joined by anyone (RoomJoinRules.PUBLIC). */ fun StateService.isPublic(): Boolean { - return getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + return getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.IsEmpty) ?.content ?.toModel() ?.joinRules == RoomJoinRules.PUBLIC diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt index 8ad6500d25..c2094f46bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.session.widgets import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.widgets.model.Widget @@ -49,7 +49,7 @@ interface WidgetService { */ fun getRoomWidgets( roomId: String, - widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetId: QueryStateEventValue, widgetTypes: Set? = null, excludedTypes: Set? = null ): List @@ -70,7 +70,7 @@ interface WidgetService { */ fun getRoomWidgetsLive( roomId: String, - widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetId: QueryStateEventValue, widgetTypes: Set? = null, excludedTypes: Set? = null ): LiveData> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index edc45fe945..5fb20bb259 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -101,7 +101,7 @@ internal class ViaParameterFinder @Inject constructor( } fun userCanInvite(userId: String, roomId: String): Boolean { - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt index ace5ee0b9a..c2310f4fda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.session.pushers +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -55,7 +56,7 @@ internal class DefaultConditionResolver @Inject constructor( val roomId = event.roomId ?: return false val room = roomGetter.getRoom(roomId) ?: return false - val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() ?: PowerLevelsContent() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index bb43d90328..24d4975eb9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -386,7 +386,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? { - return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt index 1b836e36a6..dd28bbcc73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -60,7 +60,7 @@ internal class DefaultLeaveRoomTask @Inject constructor( val roomCreateStateEvent = stateEventDataSource.getStateEvent( roomId = roomId, eventType = EventType.STATE_ROOM_CREATE, - stateKey = QueryStringValue.NoCondition + stateKey = QueryStringValue.IsEmpty, ) // Server is not cleaning predecessor rooms, so we also try to left them val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel()?.predecessor?.roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 2a980f3286..c15bcb1c1a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -22,6 +22,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -54,19 +55,19 @@ internal class DefaultStateService @AssistedInject constructor( fun create(roomId: String): DefaultStateService } - override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { + override fun getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? { return stateEventDataSource.getStateEvent(roomId, eventType, stateKey) } - override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData> { + override fun getStateEventLive(eventType: String, stateKey: QueryStateEventValue): LiveData> { return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey) } - override fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue): List { + override fun getStateEvents(eventTypes: Set, stateKey: QueryStateEventValue): List { return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey) } - override fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue): LiveData> { + override fun getStateEventsLive(eventTypes: Set, stateKey: QueryStateEventValue): LiveData> { return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt index 18c709adf2..9971ce3ccc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -22,6 +22,7 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Optional @@ -40,13 +41,13 @@ internal class StateEventDataSource @Inject constructor( private val queryStringValueProcessor: QueryStringValueProcessor ) { - fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { + fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStateEventValue): Event? { return realmSessionProvider.withRealm { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() } } - fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData> { + fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStateEventValue): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) }, { it.root?.asDomain() } @@ -56,7 +57,7 @@ internal class StateEventDataSource @Inject constructor( } } - fun getStateEvents(roomId: String, eventTypes: Set, stateKey: QueryStringValue): List { + fun getStateEvents(roomId: String, eventTypes: Set, stateKey: QueryStateEventValue): List { return realmSessionProvider.withRealm { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) .findAll() @@ -66,7 +67,7 @@ internal class StateEventDataSource @Inject constructor( } } - fun getStateEventsLive(roomId: String, eventTypes: Set, stateKey: QueryStringValue): LiveData> { + fun getStateEventsLive(roomId: String, eventTypes: Set, stateKey: QueryStateEventValue): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) }, { it.root?.asDomain() } @@ -80,7 +81,7 @@ internal class StateEventDataSource @Inject constructor( realm: Realm, roomId: String, eventTypes: Set, - stateKey: QueryStringValue + stateKey: QueryStateEventValue ): RealmQuery { return with(queryStringValueProcessor) { realm.where() @@ -90,7 +91,8 @@ internal class StateEventDataSource @Inject constructor( `in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) } } - .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + // It's OK to cast stateKey as QueryStringValue + .process(CurrentStateEventEntityFields.STATE_KEY, stateKey as QueryStringValue) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt index b2692bf805..b78ad45938 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt @@ -46,7 +46,7 @@ internal class LiveRoomStateListener( stateEventDataSource.getStateEventsLive( roomId = roomId, eventTypes = setOf(EventType.STATE_ROOM_MEMBER), - stateKey = QueryStringValue.NoCondition, + stateKey = QueryStringValue.IsNotNull, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt index dc12c3209b..0bde3a11d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt @@ -71,7 +71,7 @@ internal class DefaultRoomVersionService @AssistedInject constructor( } override fun userMayUpgradeRoom(userId: String): Boolean { - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index c08d9389a8..d2f1b3202b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -252,7 +252,7 @@ internal class DefaultSpaceService @Inject constructor( val powerLevelsEvent = stateEventDataSource.getStateEvent( roomId = parentSpaceId, eventType = EventType.STATE_ROOM_POWER_LEVELS, - stateKey = QueryStringValue.NoCondition + stateKey = QueryStringValue.IsEmpty ) val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt index 7b2edf2dbf..9c59c11345 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.session.widgets import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator import org.matrix.android.sdk.api.session.widgets.WidgetService @@ -43,7 +43,7 @@ internal class DefaultWidgetService @Inject constructor( override fun getRoomWidgets( roomId: String, - widgetId: QueryStringValue, + widgetId: QueryStateEventValue, widgetTypes: Set?, excludedTypes: Set? ): List { @@ -56,7 +56,7 @@ internal class DefaultWidgetService @Inject constructor( override fun getRoomWidgetsLive( roomId: String, - widgetId: QueryStringValue, + widgetId: QueryStateEventValue, widgetTypes: Set?, excludedTypes: Set? ): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index 3f7db93b97..37a329af34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.SessionLifecycleObserver @@ -71,7 +72,7 @@ internal class WidgetManager @Inject constructor( fun getRoomWidgetsLive( roomId: String, - widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetId: QueryStateEventValue, widgetTypes: Set? = null, excludedTypes: Set? = null ): LiveData> { @@ -88,7 +89,7 @@ internal class WidgetManager @Inject constructor( fun getRoomWidgets( roomId: String, - widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetId: QueryStateEventValue, widgetTypes: Set? = null, excludedTypes: Set? = null ): List { @@ -199,7 +200,7 @@ internal class WidgetManager @Inject constructor( val powerLevelsEvent = stateEventDataSource.getStateEvent( roomId = roomId, eventType = EventType.STATE_ROOM_POWER_LEVELS, - stateKey = QueryStringValue.NoCondition + stateKey = QueryStringValue.IsEmpty ) val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: return false return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY) diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt index 4d5fafbccc..d6aef43f72 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt @@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.launch import org.json.JSONObject +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.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -59,7 +60,7 @@ class RoomDevToolViewModel @AssistedInject constructor( init { session.getRoom(initialState.roomId) ?.flow() - ?.liveStateEvents(emptySet()) + ?.liveStateEvents(emptySet(), QueryStringValue.IsNotNull) ?.execute { async -> copy(stateEvents = async) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 0fcc44910c..07b20b4914 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -294,7 +294,7 @@ class TimelineViewModel @AssistedInject constructor( session.flow() .liveRoomWidgets( roomId = initialState.roomId, - widgetId = QueryStringValue.NoCondition + widgetId = QueryStringValue.IsNotNull ) .map { widgets -> widgets.filter { it.isActive } @@ -1239,7 +1239,7 @@ class TimelineViewModel @AssistedInject constructor( setState { copy(asyncInviter = Success(it)) } } } - room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also { + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { setState { copy(tombstoneEvent = it) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 37b17d9133..ce4235a825 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -683,7 +683,7 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { - val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 8c39584a94..771e42b63c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -222,7 +222,7 @@ class MergedHeaderItemFactory @Inject constructor( } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId) - ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel() } + ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)?.content?.toModel() } ?.let { PowerLevelsHelper(it) } val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" val attributes = MergedRoomCreationItem.Attributes( diff --git a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt index d8857b3be3..5cceb38fde 100644 --- a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt +++ b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt @@ -32,7 +32,7 @@ class PowerLevelsFlowFactory(private val room: Room) { fun createFlow(): Flow { return room.flow() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .flowOn(Dispatchers.Default) .unwrap() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 4630fb1b86..30664c5618 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -91,7 +91,7 @@ class RoomProfileViewModel @AssistedInject constructor( } private fun observeRoomCreateContent(flowRoom: FlowRoom) { - flowRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition) + flowRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .execute { async -> @@ -101,7 +101,7 @@ class RoomProfileViewModel @AssistedInject constructor( recommendedRoomVersion = room.roomVersionService().getRecommendedVersion(), isUsingUnstableRoomVersion = room.roomVersionService().isUsingUnstableRoomVersion(), canUpgradeRoom = room.roomVersionService().userMayUpgradeRoom(session.myUserId), - isTombstoned = room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE) != null + isTombstoned = room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) != null ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt index 1a6dfce940..7a4a33b3a7 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -167,7 +167,7 @@ class RoomAliasViewModel @AssistedInject constructor( */ private fun observeRoomCanonicalAlias() { room.flow() - .liveStateEvent(EventType.STATE_ROOM_CANONICAL_ALIAS, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_CANONICAL_ALIAS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .setOnEach { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 85f4f855ec..3e6fb7b9d1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -84,7 +84,7 @@ class RoomMemberListViewModel @AssistedInject constructor( combine( room.flow().liveRoomMembers(roomMemberQueryParams), room.flow() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() ) { roomMembers, powerLevelsContent -> @@ -146,7 +146,8 @@ class RoomMemberListViewModel @AssistedInject constructor( } private fun observeThirdPartyInvites() { - room.flow().liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE)) + room.flow() + .liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE), QueryStringValue.IsNotNull) .execute { async -> copy(threePidInvites = async) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index f67bea157c..501ff7553a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -158,7 +158,7 @@ class RoomSettingsViewModel @AssistedInject constructor( private fun observeRoomHistoryVisibility() { room.flow() - .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .mapNotNull { it.historyVisibility } @@ -169,7 +169,7 @@ class RoomSettingsViewModel @AssistedInject constructor( private fun observeJoinRule() { room.flow() - .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .mapNotNull { it.joinRules } @@ -180,7 +180,7 @@ class RoomSettingsViewModel @AssistedInject constructor( private fun observeGuestAccess() { room.flow() - .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .mapNotNull { it.guestAccess } @@ -194,7 +194,7 @@ class RoomSettingsViewModel @AssistedInject constructor( */ private fun observeRoomAvatar() { room.flow() - .liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition) + .liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .setOnEach { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt index 88c8dad64f..3e414e2e71 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt @@ -70,7 +70,7 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor( private fun initializeForRoom(roomId: String) { room = session.getRoom(roomId)!! session.getRoomSummary(roomId)?.let { roomSummary -> - val joinRulesContent = room.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + val joinRulesContent = room.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.IsEmpty) ?.content ?.toModel() val initialAllowList = joinRulesContent?.allowList diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt index 926739f96c..7413386709 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import okhttp3.internal.toImmutableList +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.session.Session @@ -59,7 +60,7 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor( val space = session.getRoom(initialState.spaceId) val spaceSummary = space?.roomSummary() - val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) powerLevelsEvent?.content?.toModel()?.let { powerLevelsContent -> val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 5a037ff16b..fc1defd20f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -121,7 +121,7 @@ class WidgetPostAPIHandler @AssistedInject constructor( } val userId = eventData["user_id"] as String Timber.d("Received request to get options for bot $userId in room $roomId requested") - val stateEvents = room.stateService().getStateEvents(setOf(EventType.BOT_OPTIONS)) + val stateEvents = room.stateService().getStateEvents(setOf(EventType.BOT_OPTIONS), QueryStringValue.IsNotNull) var botOptionsEvent: Event? = null val stateKey = "_$userId" for (stateEvent in stateEvents) { @@ -155,7 +155,7 @@ class WidgetPostAPIHandler @AssistedInject constructor( Timber.d("## canSendEvent() : eventType $eventType isState $isState") - val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) val powerLevelsContent = powerLevelsEvent?.content?.toModel() val canSend = if (powerLevelsContent == null) { false @@ -202,7 +202,7 @@ class WidgetPostAPIHandler @AssistedInject constructor( return } Timber.d("Received request join rules in room $roomId") - val joinedEvents = room.stateService().getStateEvents(setOf(EventType.STATE_ROOM_JOIN_RULES)) + val joinedEvents = room.stateService().getStateEvents(setOf(EventType.STATE_ROOM_JOIN_RULES), QueryStringValue.IsEmpty) if (joinedEvents.isNotEmpty()) { widgetPostAPIMediator.sendObjectResponse(Event::class.java, joinedEvents.last(), eventData) } else { @@ -222,7 +222,7 @@ class WidgetPostAPIHandler @AssistedInject constructor( } Timber.d("Received request to get widget in room $roomId") val responseData = ArrayList() - val allWidgets = session.widgetService().getRoomWidgets(roomId) + session.widgetService().getUserWidgets() + val allWidgets = session.widgetService().getRoomWidgets(roomId, QueryStringValue.IsNotNull) + session.widgetService().getUserWidgets() for (widget in allWidgets) { val map = widget.event.toContent() responseData.add(map) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 87fc38f82a..b3f4712815 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -111,7 +111,7 @@ class WidgetViewModel @AssistedInject constructor( if (room == null) { return } - room.flow().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + room.flow().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() .map { From 934d860bea7655319ff20aa4728e9b3e6a6a456e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 15 Jun 2022 21:25:44 +0300 Subject: [PATCH 108/314] Changelog added. --- changelog.d/6320.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6320.misc diff --git a/changelog.d/6320.misc b/changelog.d/6320.misc new file mode 100644 index 0000000000..7cdd41f486 --- /dev/null +++ b/changelog.d/6320.misc @@ -0,0 +1 @@ +CreatePollViewModel unit tests From ec3248d7146300de7a661e22db18e73fcdfd74cd Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 15 Jun 2022 21:34:04 +0300 Subject: [PATCH 109/314] Lint fixes. --- .../vector/app/features/poll/create/CreatePollViewStates.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt index ce27176db2..518beabd1f 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt @@ -21,13 +21,13 @@ import kotlin.random.Random object CreatePollViewStates { - val fakeRoomId = "fakeRoomId" - val fakeEventId = "fakeEventId" + const val fakeRoomId = "fakeRoomId" + const val fakeEventId = "fakeEventId" val createPollArgs = CreatePollArgs(fakeRoomId, null, PollMode.CREATE) val editPollArgs = CreatePollArgs(fakeRoomId, fakeEventId, PollMode.EDIT) - val fakeQuestion = "What is your favourite coffee?" + const val fakeQuestion = "What is your favourite coffee?" val fakeOptions = List(CreatePollViewModel.MAX_OPTIONS_COUNT + 1) { "Coffee No${Random.nextInt()}" } val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy( From d5a4e764f22bcd163640e0fc4761ca0a984e386f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Jun 2022 21:16:57 +0200 Subject: [PATCH 110/314] Changelog --- changelog.d/6319.sdk | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6319.sdk diff --git a/changelog.d/6319.sdk b/changelog.d/6319.sdk new file mode 100644 index 0000000000..3bf81c45b6 --- /dev/null +++ b/changelog.d/6319.sdk @@ -0,0 +1 @@ +Create `QueryStateEventValue` to do query on `stateKey` for State Event. Also remove the default parameter values for those type. From 3367c059e9fc956a4257f89db379a3deba9befa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jun 2022 08:40:08 +0000 Subject: [PATCH 111/314] Bump annotation from 1.3.0 to 1.4.0 Bumps annotation from 1.3.0 to 1.4.0. --- updated-dependencies: - dependency-name: androidx.annotation:annotation dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 604174fe57..7bd3b930ac 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -48,7 +48,7 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'annotation' : "androidx.annotation:annotation:1.3.0", + 'annotation' : "androidx.annotation:annotation:1.4.0", 'activity' : "androidx.activity:activity:1.4.0", 'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'core' : "androidx.core:core-ktx:1.8.0", From d978d0a6b463a8cf67acf85c90c339a59021a8e3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Jun 2022 11:56:30 +0200 Subject: [PATCH 112/314] Delete obsolete comment. --- .../im/vector/app/core/pushers/VectorMessagingReceiver.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 723d9c2480..53a5470ff7 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -119,12 +119,6 @@ class VectorMessagingReceiver : MessagingReceiver() { } } - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. Note that this is also called - * when the InstanceID token is initially generated, so this is where - * you retrieve the token. - */ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { From d8814974c56550ca96cb77c44b8bd368adecb2bb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 16 Jun 2022 10:58:28 +0100 Subject: [PATCH 113/314] makes the bug report screenshot preview always visible --- .../java/im/vector/app/features/rageshake/BugReportActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 73f82121e8..c761c498d4 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -259,7 +259,7 @@ class BugReportActivity : VectorBaseActivity() { } private fun onSendScreenshotChanged() { - views.bugReportScreenshotPreview.isVisible = views.bugReportButtonIncludeScreenshot.isChecked && bugReporter.screenshot != null + views.bugReportScreenshotPreview.isVisible = bugReporter.screenshot != null } override fun onBackPressed() { From 825f14d919309981bc1938bf258edd1f4ff461ed Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 14:06:49 +0100 Subject: [PATCH 114/314] ignoring text suggestions on username inputs, to avoid the spell checker introducing word breaks --- vector/src/main/res/layout/fragment_ftue_combined_login.xml | 2 +- vector/src/main/res/layout/fragment_ftue_combined_register.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml index 1b65056e9f..8037f207fc 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -150,7 +150,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:imeOptions="actionNext" - android:inputType="text" + android:inputType="textNoSuggestions" android:maxLines="1" android:nextFocusForward="@id/loginPasswordInput" /> diff --git a/vector/src/main/res/layout/fragment_ftue_combined_register.xml b/vector/src/main/res/layout/fragment_ftue_combined_register.xml index 9d61780ad0..304e5b475f 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_register.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_register.xml @@ -174,7 +174,7 @@ android:layout_height="match_parent" android:imeOptions="actionNext" android:nextFocusForward="@id/createAccountPasswordInput" - android:inputType="text" + android:inputType="textNoSuggestions" android:maxLines="1" /> From 17f8009ce0a92374296a169a8131146bde53729a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 14:22:12 +0100 Subject: [PATCH 115/314] only removing the edit server fragment when homeserver edits are complete --- .../app/features/onboarding/ftueauth/FtueAuthVariant.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index bae90f1960..fa37e2edce 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" +private const val FRAGMENT_EDIT_HOMESERVER_TAG = "FRAGMENT_EDIT_HOMESERVER" class FtueAuthVariant( private val views: ActivityLoginBinding, @@ -220,10 +221,14 @@ class FtueAuthVariant( activity.addFragmentToBackstack( views.loginFragmentContainer, FtueAuthCombinedServerSelectionFragment::class.java, - option = commonOption + option = commonOption, + tag = FRAGMENT_EDIT_HOMESERVER_TAG ) } - OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() + OnboardingViewEvents.OnHomeserverEdited -> supportFragmentManager.popBackStack( + FRAGMENT_EDIT_HOMESERVER_TAG, + FragmentManager.POP_BACK_STACK_INCLUSIVE + ) OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents) OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog() From 90f16c67365e9ce596236557d20720805e70c58f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Jun 2022 12:16:07 +0200 Subject: [PATCH 116/314] Remove unused dep. --- dependencies.gradle | 1 - vector/build.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 0b29996438..dbbd32e078 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,7 +43,6 @@ ext.libs = [ ], jetbrains : [ - 'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$kotlin", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" diff --git a/vector/build.gradle b/vector/build.gradle index 82f43c6ac4..eceaed390e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -348,7 +348,6 @@ dependencies { implementation project(":library:multipicker") implementation 'androidx.multidex:multidex:2.0.1' - implementation libs.jetbrains.kotlinReflect implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid From e0fe91f54b158b968d44235cb4e281466c3af146 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Jun 2022 12:17:54 +0200 Subject: [PATCH 117/314] Remove alias. --- .../app/core/pushers/UnifiedPushHelper.kt | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 2e906e2727..198900874e 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -47,8 +47,6 @@ class UnifiedPushHelper @Inject constructor( private val vectorFeatures: VectorFeatures, private val fcmHelper: FcmHelper, ) { - private val up = UnifiedPush - fun register( activity: FragmentActivity, onDoneRunnable: Runnable? = null, @@ -80,8 +78,8 @@ class UnifiedPushHelper @Inject constructor( ) { activity.lifecycleScope.launch { if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { - up.saveDistributor(context, context.packageName) - up.registerApp(context) + UnifiedPush.saveDistributor(context, context.packageName) + UnifiedPush.registerApp(context) onDoneRunnable?.run() return@launch } @@ -89,19 +87,19 @@ class UnifiedPushHelper @Inject constructor( // Un-register first unregister(pushersManager) } - if (up.getDistributor(context).isNotEmpty()) { - up.registerApp(context) + if (UnifiedPush.getDistributor(context).isNotEmpty()) { + UnifiedPush.registerApp(context) onDoneRunnable?.run() return@launch } // By default, use internal solution (fcm/background sync) - up.saveDistributor(context, context.packageName) - val distributors = up.getDistributors(context) + UnifiedPush.saveDistributor(context, context.packageName) + val distributors = UnifiedPush.getDistributors(context) if (distributors.size == 1 && !force) { - up.saveDistributor(context, distributors.first()) - up.registerApp(context) + UnifiedPush.saveDistributor(context, distributors.first()) + UnifiedPush.registerApp(context) onDoneRunnable?.run() } else { openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force) @@ -114,7 +112,7 @@ class UnifiedPushHelper @Inject constructor( pushersManager: PushersManager, onDoneRunnable: Runnable, ) { - val distributors = up.getDistributors(activity) + val distributors = UnifiedPush.getDistributors(activity) openDistributorDialogInternal( activity, pushersManager, @@ -152,7 +150,7 @@ class UnifiedPushHelper @Inject constructor( .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setItems(distributorsName.toTypedArray()) { _, which -> val distributor = distributors[which] - if (distributor == up.getDistributor(context)) { + if (distributor == UnifiedPush.getDistributor(context)) { Timber.d("Same distributor selected again, no action") return@setItems } @@ -162,9 +160,9 @@ class UnifiedPushHelper @Inject constructor( // Un-register first unregister(pushersManager) } - up.saveDistributor(context, distributor) + UnifiedPush.saveDistributor(context, distributor) Timber.i("Saving distributor: $distributor") - up.registerApp(context) + UnifiedPush.registerApp(context) onDoneRunnable?.run() } } @@ -182,7 +180,7 @@ class UnifiedPushHelper @Inject constructor( } unifiedPushStore.storeUpEndpoint(null) unifiedPushStore.storePushGateway(null) - up.unregisterApp(context) + UnifiedPush.unregisterApp(context) } @JsonClass(generateAdapter = true) @@ -202,7 +200,7 @@ class UnifiedPushHelper @Inject constructor( // if we use the embedded distributor, // register app_id type upfcm on sygnal // the pushkey if FCM key - if (up.getDistributor(context) == context.packageName) { + if (UnifiedPush.getDistributor(context) == context.packageName) { unifiedPushStore.storePushGateway(stringProvider.getString(R.string.pusher_http_url)) onDoneRunnable?.run() return @@ -232,7 +230,7 @@ class UnifiedPushHelper @Inject constructor( } fun getExternalDistributors(): List { - return up.getDistributors(context) + return UnifiedPush.getDistributors(context) .filterNot { it == context.packageName } } @@ -240,16 +238,16 @@ class UnifiedPushHelper @Inject constructor( return when { isEmbeddedDistributor() -> stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) - else -> context.getApplicationLabel(up.getDistributor(context)) + else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) } } fun isEmbeddedDistributor(): Boolean { - return up.getDistributor(context) == context.packageName && fcmHelper.isFirebaseAvailable() + return UnifiedPush.getDistributor(context) == context.packageName && fcmHelper.isFirebaseAvailable() } fun isBackgroundSync(): Boolean { - return up.getDistributor(context) == context.packageName && !fcmHelper.isFirebaseAvailable() + return UnifiedPush.getDistributor(context) == context.packageName && !fcmHelper.isFirebaseAvailable() } fun getPrivacyFriendlyUpEndpoint(): String? { From 3a97cfcc36d452a1882b9da4d20301d8bff49e00 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 15:28:31 +0100 Subject: [PATCH 118/314] updating the selected homeserver when we detect a full matrix id within the username field in the login/register input fields --- .../app/core/extensions/TextInputLayout.kt | 11 ++++ .../features/onboarding/OnboardingAction.kt | 1 + .../onboarding/OnboardingViewModel.kt | 13 ++++ .../ftueauth/FtueAuthCombinedLoginFragment.kt | 2 + .../FtueAuthCombinedRegisterFragment.kt | 4 ++ .../onboarding/OnboardingViewModelTest.kt | 59 +++++++++++++++---- 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt index c5009bd072..41016365c0 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -57,3 +57,14 @@ fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) { } } } + +fun TextInputLayout.setOnFocusLostListener(action: () -> Unit) { + editText().setOnFocusChangeListener { _, hasFocus -> + when (hasFocus) { + false -> action() + else -> { + // do nothing + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index bd2ff1a26a..b6a7550a58 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -50,6 +50,7 @@ sealed interface OnboardingAction : VectorViewModelAction { data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction object ResetPasswordMailConfirmed : OnboardingAction + data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction sealed interface AuthenticateAction : OnboardingAction { data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index c41c9717f5..50e68dd324 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -50,6 +50,8 @@ import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAut import kotlinx.coroutines.Job import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -142,6 +144,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) is OnboardingAction.InitWith -> handleInitWith(action) is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) } + is OnboardingAction.MaybeUpdateHomeserverFromMatrixId -> handleMaybeUpdateHomeserver(action) is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) } is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) @@ -162,6 +165,16 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun handleMaybeUpdateHomeserver(action: OnboardingAction.MaybeUpdateHomeserverFromMatrixId) { + val isFullMatrixId = MatrixPatterns.isUserId(action.userId) + if (isFullMatrixId) { + val domain = action.userId.getServerName().substringBeforeLast(":").ensureProtocol() + handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain)) + } else { + // ignore the action + } + } + private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) { lastAction = action block(action) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index 10b9cf4683..205a604aab 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.setOnFocusLostListener import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueCombinedLoginBinding @@ -59,6 +60,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor( views.loginRoot.realignPercentagesToParent() views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.loginPasswordInput.setOnImeDoneListener { submit() } + views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) } } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index e19f7837c3..6918a3682f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -34,6 +34,7 @@ import im.vector.app.core.extensions.hasSurroundingSpaces import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.setOnFocusLostListener import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding @@ -67,6 +68,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu views.createAccountRoot.realignPercentagesToParent() views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.createAccountPasswordInput.setOnImeDoneListener { submit() } + views.createAccountInput.setOnFocusLostListener { + viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content())) + } } private fun setupSubmitButton() { diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 658e14d411..c5d24c0ec3 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -271,10 +271,7 @@ class OnboardingViewModelTest { @Test fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest { viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) - fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG) - fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE)) - givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT) - fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) + givenCanSuccessfullyUpdateHomeserver(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE) val test = viewModel.test() viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)) @@ -291,13 +288,45 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given a full matrix id, when maybe updating homeserver, then updates selected homeserver state and emits edited event`() = runTest { + viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) + givenCanSuccessfullyUpdateHomeserver(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE) + val test = viewModel.test() + val fullMatrixId = "@a-user:${A_HOMESERVER_URL.removePrefix("https://")}" + + viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(fullMatrixId)) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) }, + { copy(isLoading = false) } + + ) + .assertEvents(OnboardingViewEvents.OnHomeserverEdited) + .finish() + } + + @Test + fun `given a username, when maybe updating homeserver, then does nothing`() = runTest { + viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) + val test = viewModel.test() + val onlyUsername = "a-username" + + viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(onlyUsername)) + + test + .assertStates(initialState) + .assertNoEvents() + .finish() + } + @Test fun `given in the sign up flow, when editing homeserver errors, then does not update the selected homeserver state and emits error`() = runTest { viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) - fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG) - fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE)) - givenRegistrationActionErrors(RegisterAction.StartRegistration, AN_ERROR) - fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) + givenUpdatingHomeserverErrors(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE, AN_ERROR) val test = viewModel.test() viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)) @@ -552,8 +581,18 @@ class OnboardingViewModelTest { fakeRegistrationActionHandler.givenResultsFor(results) } - private fun givenRegistrationActionErrors(action: RegisterAction, cause: Throwable) { - fakeRegistrationActionHandler.givenThrows(action, cause) + private fun givenCanSuccessfullyUpdateHomeserver(homeserverUrl: String, resultingState: SelectedHomeserverState) { + fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG) + fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) + givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT) + fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) + } + + private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { + fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG) + fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) + givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) + fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) } } From f89b9305e89a1d96d00c9b61bd34022e04ea0be9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 15:33:46 +0100 Subject: [PATCH 119/314] handling the unavailable homeserver error case in the error formatting as this is now possible via full matrix id handling# --- .../app/features/onboarding/ftueauth/LoginErrorParser.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt index 271c1ced14..ac79419312 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt @@ -20,6 +20,7 @@ import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult +import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isLoginEmailUnknown @@ -40,6 +41,9 @@ class LoginErrorParser @Inject constructor( throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> { LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password)) } + throwable.isHomeserverUnavailable() -> { + LoginErrorResult(throwable, usernameOrIdError = stringProvider.getString(R.string.login_error_homeserver_not_found)) + } else -> { LoginErrorResult(throwable) } From b25fd4a5406c93fbf7c70abaddfe3d7c1049c6b8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 16:08:17 +0100 Subject: [PATCH 120/314] adding tests around the login error parsing --- .../ftueauth/LoginErrorParserTest.kt | 102 ++++++++++++++++++ .../app/test/fakes/FakeErrorFormatter.kt | 27 +++++ .../app/test/fixtures/FailureFixture.kt | 12 +++ 3 files changed, 141 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeErrorFormatter.kt diff --git a/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt new file mode 100644 index 0000000000..9c7859685d --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt @@ -0,0 +1,102 @@ +/* + * 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.onboarding.ftueauth + +import im.vector.app.R +import im.vector.app.test.fakes.FakeErrorFormatter +import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.fakes.toTestString +import im.vector.app.test.fixtures.aHomeserverUnavailableError +import im.vector.app.test.fixtures.aLoginEmailUnknownError +import im.vector.app.test.fixtures.anInvalidPasswordError +import im.vector.app.test.fixtures.anInvalidUserNameError +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private const val A_VALID_PASSWORD = "11111111" +private const val A_FORMATTED_ERROR_MESSAGE = "error message" + +class LoginErrorParserTest { + + private val fakeErrorFormatter = FakeErrorFormatter() + private val fakeStringProvider = FakeStringProvider() + + private val loginErrorParser = LoginErrorParser(fakeErrorFormatter, fakeStringProvider.instance) + + @Test + fun `given a generic error, when parsing, then has null username and password errors`() { + val cause = RuntimeException() + + val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD) + + result shouldBeEqualTo LoginErrorParser.LoginErrorResult(cause, usernameOrIdError = null, passwordError = null) + } + + @Test + fun `given an invalid username error, when parsing, then has username error`() { + val cause = anInvalidUserNameError() + fakeErrorFormatter.given(cause, formatsTo = A_FORMATTED_ERROR_MESSAGE) + + val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD) + + result shouldBeEqualTo LoginErrorParser.LoginErrorResult( + cause, + usernameOrIdError = A_FORMATTED_ERROR_MESSAGE, + passwordError = null + ) + } + + @Test + fun `given a homeserver unavailable error, when parsing, then has username error`() { + val cause = aHomeserverUnavailableError() + + val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD) + + result shouldBeEqualTo LoginErrorParser.LoginErrorResult( + cause, + usernameOrIdError = R.string.login_error_homeserver_not_found.toTestString(), + passwordError = null + ) + } + + @Test + fun `given a login email unknown error, when parsing, then has username error`() { + val cause = aLoginEmailUnknownError() + + val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD) + + result shouldBeEqualTo LoginErrorParser.LoginErrorResult( + cause, + usernameOrIdError = R.string.login_login_with_email_error.toTestString(), + passwordError = null + ) + } + + @Test + fun `given a password with surrounding spaces and an invalid password error, when parsing, then has password error`() { + val cause = anInvalidPasswordError() + + val result = loginErrorParser.parse(throwable = cause, password = " $A_VALID_PASSWORD ") + + result shouldBeEqualTo LoginErrorParser.LoginErrorResult( + cause, + usernameOrIdError = null, + passwordError = R.string.auth_invalid_login_param_space_in_password.toTestString() + ) + } +} + diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeErrorFormatter.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeErrorFormatter.kt new file mode 100644 index 0000000000..98c554b5af --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeErrorFormatter.kt @@ -0,0 +1,27 @@ +/* + * 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.error.ErrorFormatter +import io.mockk.every +import io.mockk.mockk + +class FakeErrorFormatter : ErrorFormatter by mockk() { + fun given(cause: Throwable, formatsTo: String) { + every { toHumanReadable(cause) } returns formatsTo + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt index 9ac851ef5e..0f44976ab3 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt @@ -25,4 +25,16 @@ fun a401ServerError() = Failure.ServerError( MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED ) +fun anInvalidUserNameError() = Failure.ServerError( + MatrixError(MatrixError.M_INVALID_USERNAME, ""), HttpsURLConnection.HTTP_BAD_REQUEST +) + +fun anInvalidPasswordError() = Failure.ServerError( + MatrixError(MatrixError.M_FORBIDDEN, "Invalid password"), HttpsURLConnection.HTTP_FORBIDDEN +) + +fun aLoginEmailUnknownError() = Failure.ServerError( + MatrixError(MatrixError.M_FORBIDDEN, ""), HttpsURLConnection.HTTP_FORBIDDEN +) + fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException()) From d71d37c1ce414164f40ec8f4ef20ef39c672fa3a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 16:33:17 +0100 Subject: [PATCH 121/314] adding tests around the result _on_ helper callbacks --- .../ftueauth/LoginErrorParserTest.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt index 9c7859685d..f36600e75f 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParserTest.kt @@ -29,6 +29,8 @@ import org.junit.Test private const val A_VALID_PASSWORD = "11111111" private const val A_FORMATTED_ERROR_MESSAGE = "error message" +private const val ANOTHER_FORMATTED_ERROR_MESSAGE = "error message 2" +private val AN_ERROR = RuntimeException() class LoginErrorParserTest { @@ -98,5 +100,75 @@ class LoginErrorParserTest { passwordError = R.string.auth_invalid_login_param_space_in_password.toTestString() ) } + + @Test + fun `given an error result with no known errors, then is unknown`() { + val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = null, passwordError = null) + val captures = Captures(expectUnknownError = true) + + errorResult.callOnMethods(captures) + + captures.unknownResult shouldBeEqualTo AN_ERROR + } + + @Test + fun `given an error result with only username error, then is username or id error`() { + val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = A_FORMATTED_ERROR_MESSAGE, passwordError = null) + val captures = Captures(expectUsernameOrIdError = true) + + errorResult.callOnMethods(captures) + + captures.usernameOrIdError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE + } + + @Test + fun `given an error result with only password error, then is password error`() { + val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = null, passwordError = A_FORMATTED_ERROR_MESSAGE) + val captures = Captures(expectPasswordError = true) + + errorResult.callOnMethods(captures) + + captures.passwordError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE + } + + @Test + fun `given an error result with username and password error, then triggers both username and password error`() { + val errorResult = LoginErrorParser.LoginErrorResult( + AN_ERROR, + usernameOrIdError = A_FORMATTED_ERROR_MESSAGE, + passwordError = ANOTHER_FORMATTED_ERROR_MESSAGE + ) + val captures = Captures(expectPasswordError = true, expectUsernameOrIdError = true) + + errorResult.callOnMethods(captures) + + captures.usernameOrIdError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE + captures.passwordError shouldBeEqualTo ANOTHER_FORMATTED_ERROR_MESSAGE + } } +private fun LoginErrorParser.LoginErrorResult.callOnMethods(captures: Captures) { + onUnknown(captures.onUnknown) + onUsernameOrIdError(captures.onUsernameOrIdError) + onPasswordError(captures.onPasswordError) +} + +private class Captures( + val expectUnknownError: Boolean = false, + val expectUsernameOrIdError: Boolean = false, + val expectPasswordError: Boolean = false, +) { + var unknownResult: Throwable? = null + var usernameOrIdError: String? = null + var passwordError: String? = null + + val onUnknown: (Throwable) -> Unit = { + if (expectUnknownError) unknownResult = it else throw IllegalStateException("Not expected to be called") + } + val onUsernameOrIdError: (String) -> Unit = { + if (expectUsernameOrIdError) usernameOrIdError = it else throw IllegalStateException("Not expected to be called") + } + val onPasswordError: (String) -> Unit = { + if (expectPasswordError) passwordError = it else throw IllegalStateException("Not expected to be called") + } +} From c6bae6812d61601d65f228fd0f740ab4b212cf99 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 16:48:15 +0100 Subject: [PATCH 122/314] adding unavailable homeserver error messaging in the registration page --- .../onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 6918a3682f..7df1940970 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -48,6 +48,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.launchIn import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isLoginEmailUnknown @@ -133,6 +134,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu throwable.isWeakPassword() || throwable.isInvalidPassword() -> { views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable) } + throwable.isHomeserverUnavailable() -> { + views.createAccountInput.error = getString(R.string.login_error_homeserver_not_found) + } throwable.isRegistrationDisabled() -> { MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.dialog_title_error) From 19de43dd65ae21c3d1a68532a722eb3dd667f09b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jun 2022 16:48:21 +0100 Subject: [PATCH 123/314] adding changelog entry --- changelog.d/6162.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6162.wip diff --git a/changelog.d/6162.wip b/changelog.d/6162.wip new file mode 100644 index 0000000000..8b32a72571 --- /dev/null +++ b/changelog.d/6162.wip @@ -0,0 +1 @@ +FTUE - Adds automatic homeserver selection when typing a full matrix id during registration or login From 30f5e2bb6c74de7a40bcbebbb7df2f7467baadd3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 16 Jun 2022 10:48:29 +0100 Subject: [PATCH 124/314] adding test around matrix user id check --- .../android/sdk/api/MatrixPatternsTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt new file mode 100644 index 0000000000..f9920e39c7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt @@ -0,0 +1,42 @@ +/* + * 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 + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class MatrixPatternsTest { + + @Test + fun `given user id cases, when checking isUserId, then returns expected`() { + val cases = listOf( + UserIdCase("foobar", isUserId = false), + UserIdCase("@foobar", isUserId = false), + UserIdCase("foobar@matrix.org", isUserId = false), + UserIdCase("@foobar: matrix.org", isUserId = false), + UserIdCase("@foobar:matrix.org", isUserId = true), + ) + + cases.forEach { (input, expected) -> + MatrixPatterns.isUserId(input) shouldBeEqualTo expected + } + } + +} + +private data class UserIdCase(val input: String, val isUserId: Boolean) From 6a66125286090522ca5609b20a8abb8d216009fe Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 16 Jun 2022 11:19:14 +0100 Subject: [PATCH 125/314] formatting --- .../android/sdk/api/MatrixPatternsTest.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt index f9920e39c7..eff1b14106 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt @@ -24,19 +24,18 @@ class MatrixPatternsTest { @Test fun `given user id cases, when checking isUserId, then returns expected`() { - val cases = listOf( - UserIdCase("foobar", isUserId = false), - UserIdCase("@foobar", isUserId = false), - UserIdCase("foobar@matrix.org", isUserId = false), - UserIdCase("@foobar: matrix.org", isUserId = false), - UserIdCase("@foobar:matrix.org", isUserId = true), - ) + val cases = listOf( + UserIdCase("foobar", isUserId = false), + UserIdCase("@foobar", isUserId = false), + UserIdCase("foobar@matrix.org", isUserId = false), + UserIdCase("@foobar: matrix.org", isUserId = false), + UserIdCase("@foobar:matrix.org", isUserId = true), + ) - cases.forEach { (input, expected) -> - MatrixPatterns.isUserId(input) shouldBeEqualTo expected - } + cases.forEach { (input, expected) -> + MatrixPatterns.isUserId(input) shouldBeEqualTo expected + } } - } private data class UserIdCase(val input: String, val isUserId: Boolean) From 94c0a020fb45b38e2656d33a267d504a12e2901f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 13:39:32 +0300 Subject: [PATCH 126/314] Rename test data class. --- .../poll/create/CreatePollViewStates.kt | 71 ---------- .../poll/create/FakeCreatePollViewStates.kt | 122 ++++++++++++++++++ 2 files changed, 122 insertions(+), 71 deletions(-) delete mode 100644 vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt create mode 100644 vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt deleted file mode 100644 index 518beabd1f..0000000000 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewStates.kt +++ /dev/null @@ -1,71 +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.poll.create - -import im.vector.app.features.poll.PollMode -import kotlin.random.Random - -object CreatePollViewStates { - - const val fakeRoomId = "fakeRoomId" - const val fakeEventId = "fakeEventId" - - val createPollArgs = CreatePollArgs(fakeRoomId, null, PollMode.CREATE) - val editPollArgs = CreatePollArgs(fakeRoomId, fakeEventId, PollMode.EDIT) - - const val fakeQuestion = "What is your favourite coffee?" - val fakeOptions = List(CreatePollViewModel.MAX_OPTIONS_COUNT + 1) { "Coffee No${Random.nextInt()}" } - - val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy( - canCreatePoll = false, - canAddMoreOptions = true - ) - - val pollViewStateWithOnlyQuestion = initialCreatePollViewState.copy( - question = fakeQuestion, - canCreatePoll = false, - canAddMoreOptions = true - ) - - val pollViewStateWithQuestionAndNotEnoughOptions = initialCreatePollViewState.copy( - question = fakeQuestion, - options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT - 1).toMutableList().apply { add("") }, - canCreatePoll = false, - canAddMoreOptions = true - ) - - val pollViewStateWithoutQuestionAndEnoughOptions = initialCreatePollViewState.copy( - question = "", - options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT), - canCreatePoll = false, - canAddMoreOptions = true - ) - - val pollViewStateWithQuestionAndEnoughOptions = initialCreatePollViewState.copy( - question = fakeQuestion, - options = fakeOptions.take(CreatePollViewModel.MIN_OPTIONS_COUNT), - canCreatePoll = true, - canAddMoreOptions = true - ) - - val pollViewStateWithQuestionAndMaxOptions = initialCreatePollViewState.copy( - question = fakeQuestion, - options = fakeOptions.take(CreatePollViewModel.MAX_OPTIONS_COUNT), - canCreatePoll = true, - canAddMoreOptions = false - ) -} diff --git a/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt new file mode 100644 index 0000000000..b384a9a257 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt @@ -0,0 +1,122 @@ +/* + * 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.poll.create + +import im.vector.app.features.poll.PollMode +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import kotlin.random.Random + +object FakeCreatePollViewStates { + + const val A_FAKE_ROOM_ID = "fakeRoomId" + private const val A_FAKE_EVENT_ID = "fakeEventId" + private const val A_FAKE_USER_ID = "fakeUserId" + + val createPollArgs = CreatePollArgs(A_FAKE_ROOM_ID, null, PollMode.CREATE) + val editPollArgs = CreatePollArgs(A_FAKE_ROOM_ID, A_FAKE_EVENT_ID, PollMode.EDIT) + + const val A_FAKE_QUESTION = "What is your favourite coffee?" + val A_FAKE_OPTIONS = List(CreatePollViewModel.MAX_OPTIONS_COUNT + 1) { "Coffee No${Random.nextInt()}" } + + private val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = A_FAKE_QUESTION + ), + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = A_FAKE_OPTIONS[0] + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = A_FAKE_OPTIONS[1] + ) + ) + ) + ) + + private val A_POLL_START_EVENT = Event( + type = EventType.POLL_START.first(), + eventId = A_FAKE_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_FAKE_USER_ID, + roomId = A_FAKE_ROOM_ID, + content = A_POLL_CONTENT.toContent() + ) + + val A_POLL_START_TIMELINE_EVENT = TimelineEvent( + root = A_POLL_START_EVENT, + localId = 12345, + eventId = A_FAKE_EVENT_ID, + displayIndex = 1, + senderInfo = SenderInfo(A_FAKE_USER_ID, isUniqueDisplayName = true, avatarUrl = "", displayName = "") + ) + + val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy( + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithOnlyQuestion = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndNotEnoughOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT - 1).toMutableList().apply { add("") }, + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithoutQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = "", + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = false, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndEnoughOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = true + ) + + val pollViewStateWithQuestionAndMaxOptions = initialCreatePollViewState.copy( + question = A_FAKE_QUESTION, + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MAX_OPTIONS_COUNT), + canCreatePoll = true, + canAddMoreOptions = false + ) + + val editedPollViewState = pollViewStateWithQuestionAndEnoughOptions.copy( + editedEventId = A_FAKE_EVENT_ID, + mode = PollMode.EDIT + ) +} From b558d14a48be81ce3791767713f2451c9d814fd1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 13:40:00 +0300 Subject: [PATCH 127/314] Create fake room services. --- .../app/test/fakes/FakeRelationService.kt | 30 +++++++++++++++++++ .../java/im/vector/app/test/fakes/FakeRoom.kt | 9 ++++++ .../vector/app/test/fakes/FakeSendService.kt | 29 ++++++++++++++++++ .../app/test/fakes/FakeTimelineService.kt | 29 ++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt new file mode 100644 index 0000000000..828e3a25b6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRelationService.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable + +class FakeRelationService : RelationService by mockk() { + + private val cancelable = mockk() + + override fun editPoll(targetEvent: TimelineEvent, pollType: PollType, question: String, options: List): Cancelable = cancelable +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index ff87ab0fde..865b01551a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -21,7 +21,16 @@ import org.matrix.android.sdk.api.session.room.Room class FakeRoom( private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(), + private val fakeSendService: FakeSendService = FakeSendService(), + private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), + private val fakeRelationService: FakeRelationService = FakeRelationService(), ) : Room by mockk() { override fun locationSharingService() = fakeLocationSharingService + + override fun sendService() = fakeSendService + + override fun timelineService() = fakeTimelineService + + override fun relationService() = fakeRelationService } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt new file mode 100644 index 0000000000..04b9b95ce1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.util.Cancelable + +class FakeSendService : SendService by mockk() { + + private val cancelable = mockk() + + override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable = cancelable +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt new file mode 100644 index 0000000000..56f38724b1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineService + +class FakeTimelineService : TimelineService by mockk() { + + fun givenTimelineEvent(event: TimelineEvent) { + every { getTimelineEvent(any()) } returns event + } +} From 841b63b8193d919e624066420a9f834e0c8e350b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 13:40:39 +0300 Subject: [PATCH 128/314] Test poll view events when poll is edited. --- .../poll/create/CreatePollViewModelTest.kt | 98 ++++++++++++------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index fa24d9c4a6..f73175faa3 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -18,42 +18,38 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.poll.PollMode -import im.vector.app.features.poll.create.CreatePollViewStates.createPollArgs -import im.vector.app.features.poll.create.CreatePollViewStates.editPollArgs -import im.vector.app.features.poll.create.CreatePollViewStates.fakeOptions -import im.vector.app.features.poll.create.CreatePollViewStates.fakeQuestion -import im.vector.app.features.poll.create.CreatePollViewStates.fakeRoomId -import im.vector.app.features.poll.create.CreatePollViewStates.initialCreatePollViewState -import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithOnlyQuestion -import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions -import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndMaxOptions -import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions -import im.vector.app.features.poll.create.CreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions +import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_OPTIONS +import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_QUESTION +import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_ROOM_ID +import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_POLL_START_TIMELINE_EVENT +import im.vector.app.features.poll.create.FakeCreatePollViewStates.createPollArgs +import im.vector.app.features.poll.create.FakeCreatePollViewStates.editPollArgs +import im.vector.app.features.poll.create.FakeCreatePollViewStates.editedPollViewState +import im.vector.app.features.poll.create.FakeCreatePollViewStates.initialCreatePollViewState +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithOnlyQuestion +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndMaxOptions +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions +import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test -import io.mockk.every -import io.mockk.mockk +import io.mockk.unmockkAll import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.message.PollType -import org.matrix.android.sdk.api.session.room.send.SendService -import org.matrix.android.sdk.api.util.Cancelable class CreatePollViewModelTest { @get:Rule val mvrxTestRule = MvRxTestRule() - private val fakeSession = mockk() - private val fakeRoom = mockk() - private val fakeSendService = mockk() - private val fakeCancellable = mockk() + private val fakeSession = FakeSession() private fun createPollViewModel(pollMode: PollMode): CreatePollViewModel { return if (pollMode == PollMode.EDIT) { @@ -65,9 +61,16 @@ class CreatePollViewModelTest { @Before fun setup() { - every { fakeSession.getRoom(fakeRoomId) } returns fakeRoom - every { fakeRoom.sendService() } returns fakeSendService - every { fakeSendService.sendPoll(any(), fakeQuestion, any()) } returns fakeCancellable + fakeSession + .roomService() + .getRoom(A_FAKE_ROOM_ID) + .timelineService() + .givenTimelineEvent(A_POLL_START_TIMELINE_EVENT) + } + + @After + fun tearDown() { + unmockkAll() } @Test @@ -82,7 +85,7 @@ class CreatePollViewModelTest { @Test fun `given there is not any options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) // We need to wait for createPollViewModel.onChange is triggered delay(10) @@ -95,9 +98,9 @@ class CreatePollViewModelTest { @Test fun `given there is not enough options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT - 1) { - createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } delay(10) @@ -111,7 +114,7 @@ class CreatePollViewModelTest { fun `given there is not a question when enough options are added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { - createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } delay(10) @@ -124,9 +127,9 @@ class CreatePollViewModelTest { @Test fun `given there is a question when enough options are added then poll can be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { - createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } delay(10) @@ -139,12 +142,12 @@ class CreatePollViewModelTest { @Test fun `given there is a question when max number of options are added then poll can be created and more options cannot be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MAX_OPTIONS_COUNT) { if (it >= CreatePollViewModel.MIN_OPTIONS_COUNT) { createPollViewModel.handle(CreatePollAction.OnAddOption) } - createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, fakeOptions[it])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } delay(10) @@ -170,10 +173,10 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnCreatePoll) - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) createPollViewModel.handle(CreatePollAction.OnCreatePoll) - createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, fakeOptions[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) createPollViewModel.handle(CreatePollAction.OnCreatePoll) test @@ -189,9 +192,30 @@ class CreatePollViewModelTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) val test = createPollViewModel.test() - createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion)) - createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, fakeOptions[0])) - createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, fakeOptions[1])) + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, A_FAKE_OPTIONS[1])) + createPollViewModel.handle(CreatePollAction.OnCreatePoll) + + test + .assertEvents( + CreatePollViewEvents.Success + ) + } + + @Test + fun `given an edited poll event when question and options are changed then view state is updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.EDIT) + + delay(10) + createPollViewModel.test().assertState(editedPollViewState) + } + + @Test + fun `given an edited poll event then able to be edited`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.EDIT) + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnCreatePoll) test From f13dfb829169dbd70ec26b7572b2a258869428a7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 14:11:17 +0300 Subject: [PATCH 129/314] Test poll view state when poll option is deleted. --- .../poll/create/CreatePollViewModelTest.kt | 15 ++++++++++++++- .../poll/create/FakeCreatePollViewStates.kt | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index f73175faa3..e0364b286f 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -28,6 +28,7 @@ import im.vector.app.features.poll.create.FakeCreatePollViewStates.editedPollVie import im.vector.app.features.poll.create.FakeCreatePollViewStates.initialCreatePollViewState import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithOnlyQuestion import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions +import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndMaxOptions import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions @@ -41,7 +42,6 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.message.PollType class CreatePollViewModelTest { @@ -203,6 +203,19 @@ class CreatePollViewModelTest { ) } + @Test + fun `given there is a question and enough options when the last option is deleted then view state should be updated accordingly`() = runTest { + val createPollViewModel = createPollViewModel(PollMode.CREATE) + + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) + createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, A_FAKE_OPTIONS[1])) + createPollViewModel.handle(CreatePollAction.OnDeleteOption(1)) + + delay(10) + createPollViewModel.test().assertState(pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption) + } + @Test fun `given an edited poll event when question and options are changed then view state is updated accordingly`() = runTest { val createPollViewModel = createPollViewModel(PollMode.EDIT) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt index b384a9a257..e5eeb86b04 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt @@ -108,6 +108,12 @@ object FakeCreatePollViewStates { canAddMoreOptions = true ) + val pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption = pollViewStateWithQuestionAndEnoughOptions.copy( + options = A_FAKE_OPTIONS.take(CreatePollViewModel.MIN_OPTIONS_COUNT).toMutableList().apply { removeLast() }, + canCreatePoll = false, + canAddMoreOptions = true + ) + val pollViewStateWithQuestionAndMaxOptions = initialCreatePollViewState.copy( question = A_FAKE_QUESTION, options = A_FAKE_OPTIONS.take(CreatePollViewModel.MAX_OPTIONS_COUNT), From a1d35ae9e41c33df449c6cf1b9918dabc50337b0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 14:23:25 +0300 Subject: [PATCH 130/314] Move fake class into the right package. --- .../poll/create/CreatePollViewModelTest.kt | 28 +++++++++---------- .../fakes}/FakeCreatePollViewStates.kt | 5 +++- 2 files changed, 18 insertions(+), 15 deletions(-) rename vector/src/test/java/im/vector/app/{features/poll/create => test/fakes}/FakeCreatePollViewStates.kt (96%) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index e0364b286f..373fd77d40 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -18,20 +18,20 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.poll.PollMode -import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_OPTIONS -import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_QUESTION -import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_FAKE_ROOM_ID -import im.vector.app.features.poll.create.FakeCreatePollViewStates.A_POLL_START_TIMELINE_EVENT -import im.vector.app.features.poll.create.FakeCreatePollViewStates.createPollArgs -import im.vector.app.features.poll.create.FakeCreatePollViewStates.editPollArgs -import im.vector.app.features.poll.create.FakeCreatePollViewStates.editedPollViewState -import im.vector.app.features.poll.create.FakeCreatePollViewStates.initialCreatePollViewState -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithOnlyQuestion -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndMaxOptions -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions -import im.vector.app.features.poll.create.FakeCreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_OPTIONS +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_QUESTION +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_FAKE_ROOM_ID +import im.vector.app.test.fakes.FakeCreatePollViewStates.A_POLL_START_TIMELINE_EVENT +import im.vector.app.test.fakes.FakeCreatePollViewStates.createPollArgs +import im.vector.app.test.fakes.FakeCreatePollViewStates.editPollArgs +import im.vector.app.test.fakes.FakeCreatePollViewStates.editedPollViewState +import im.vector.app.test.fakes.FakeCreatePollViewStates.initialCreatePollViewState +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithOnlyQuestion +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndMaxOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithQuestionAndNotEnoughOptions +import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithoutQuestionAndEnoughOptions import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test import io.mockk.unmockkAll diff --git a/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt similarity index 96% rename from vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt index e5eeb86b04..04f3526602 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/FakeCreatePollViewStates.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt @@ -14,9 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.poll.create +package im.vector.app.test.fakes import im.vector.app.features.poll.PollMode +import im.vector.app.features.poll.create.CreatePollArgs +import im.vector.app.features.poll.create.CreatePollViewModel +import im.vector.app.features.poll.create.CreatePollViewState import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent From 7bd2184b26a1322d1b4c20cd491ba600be2f89ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Jun 2022 13:42:48 +0200 Subject: [PATCH 131/314] Rename fun. --- .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 198900874e..724d3c7aa6 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -51,7 +51,7 @@ class UnifiedPushHelper @Inject constructor( activity: FragmentActivity, onDoneRunnable: Runnable? = null, ) { - gRegister( + registerInternal( activity, onDoneRunnable = onDoneRunnable ) @@ -62,7 +62,7 @@ class UnifiedPushHelper @Inject constructor( pushersManager: PushersManager, onDoneRunnable: Runnable? = null ) { - gRegister( + registerInternal( activity, force = true, pushersManager = pushersManager, @@ -70,7 +70,7 @@ class UnifiedPushHelper @Inject constructor( ) } - private fun gRegister( + private fun registerInternal( activity: FragmentActivity, force: Boolean = false, pushersManager: PushersManager? = null, From 7558d71ec2e25b9e82637f719712857ee1907f75 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 16 Jun 2022 12:47:40 +0100 Subject: [PATCH 132/314] removing extra blank line --- .../test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt index eff1b14106..0d0450adc2 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.matrix.android.sdk.api import org.amshove.kluent.shouldBeEqualTo From 3557121758c19eba38e8a0585845990f0c74b012 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Jun 2022 14:13:03 +0200 Subject: [PATCH 133/314] Fix test compilation --- .../sdk/session/space/SpaceCreationTest.kt | 29 +++++++++++++------ .../sdk/session/space/SpaceHierarchyTest.kt | 7 +++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 0cc0ef57c4..38136ff5ce 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -28,6 +28,7 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.getStateEvent @@ -73,27 +74,37 @@ class SpaceCreationTest : InstrumentedTest { // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") assertNotNull("Space should be found by Id", syncedSpace) - val creationEvent = syncedSpace!!.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE) - val createContent = creationEvent?.content.toModel() + val createContent = syncedSpace!!.asRoom() + .getStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) + ?.content + ?.toModel() assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) var powerLevelsContent: PowerLevelsContent? = null commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { - val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel() - powerLevelsContent = toModel - toModel != null + powerLevelsContent = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + powerLevelsContent != null } } assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) - val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content - ?.toModel()?.guestAccess + val guestAccess = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.guestAccess assertEquals("Public space room should be peekable by guest", GuestAccess.CanJoin, guestAccess) - val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content - ?.toModel()?.historyVisibility + val historyVisibility = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.historyVisibility assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 5396251438..63ca963479 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -569,8 +569,9 @@ class SpaceHierarchyTest : InstrumentedTest { commonTestHelper.waitWithLatch { val room = bobSession.getRoom(bobRoomId)!! val currentPLContent = room - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) - ?.let { it.content.toModel() } + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + .toModel() val newPowerLevelsContent = currentPLContent ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) @@ -583,7 +584,7 @@ class SpaceHierarchyTest : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() ?.let { PowerLevelsHelper(it) } From 14a4a8edd79c5a50a737ef376c93e0e04956acea Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 17:30:17 +0300 Subject: [PATCH 134/314] Workaround for mavericks bug (https://github.com/airbnb/mavericks/issues/599). --- .../features/poll/create/CreatePollViewModelTest.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 373fd77d40..16f6eeffbb 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -36,6 +36,7 @@ import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test import io.mockk.unmockkAll import kotlinx.coroutines.delay +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.junit.After @@ -47,7 +48,9 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType class CreatePollViewModelTest { @get:Rule - val mvrxTestRule = MvRxTestRule() + val mvrxTestRule = MvRxTestRule( + testDispatcher = UnconfinedTestDispatcher() // See https://github.com/airbnb/mavericks/issues/599 + ) private val fakeSession = FakeSession() @@ -132,9 +135,8 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } - delay(10) - createPollViewModel - .test() + //delay(10) + createPollViewModel.test() .assertState(pollViewStateWithQuestionAndEnoughOptions) .finish() } From 712a38e26a2c4d47874ed38878b94485ec618f3b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 16 Jun 2022 18:27:00 +0300 Subject: [PATCH 135/314] Apply Maxime's fix to get latest state. --- .../poll/create/CreatePollViewModelTest.kt | 85 +++++++++++-------- .../java/im/vector/app/test/Extensions.kt | 5 ++ .../im/vector/app/test/FlowTestObserver.kt | 4 + 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 16f6eeffbb..0387fc8986 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -35,10 +35,8 @@ import im.vector.app.test.fakes.FakeCreatePollViewStates.pollViewStateWithoutQue import im.vector.app.test.fakes.FakeSession import im.vector.app.test.test import io.mockk.unmockkAll -import kotlinx.coroutines.delay import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBe import org.junit.After import org.junit.Before import org.junit.Rule @@ -47,9 +45,11 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType class CreatePollViewModelTest { + private val testDispatcher = UnconfinedTestDispatcher() + @get:Rule - val mvrxTestRule = MvRxTestRule( - testDispatcher = UnconfinedTestDispatcher() // See https://github.com/airbnb/mavericks/issues/599 + val mvRxTestRule = MvRxTestRule( + testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 ) private val fakeSession = FakeSession() @@ -79,71 +79,74 @@ class CreatePollViewModelTest { @Test fun `given the view model is initialized then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel - .test() - .assertState(initialCreatePollViewState) + val test = createPollViewModel.test() + + test + .assertLatestState(initialCreatePollViewState) .finish() } @Test fun `given there is not any options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) - // We need to wait for createPollViewModel.onChange is triggered - delay(10) - createPollViewModel - .test() - .assertState(pollViewStateWithOnlyQuestion) + test + .assertLatestState(pollViewStateWithOnlyQuestion) .finish() } @Test fun `given there is not enough options when the question is added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT - 1) { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } - delay(10) - createPollViewModel - .test() - .assertState(pollViewStateWithQuestionAndNotEnoughOptions) + test + .assertLatestState(pollViewStateWithQuestionAndNotEnoughOptions) .finish() } @Test fun `given there is not a question when enough options are added then poll cannot be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } - delay(10) - createPollViewModel - .test() - .assertState(pollViewStateWithoutQuestionAndEnoughOptions) + test + .assertLatestState(pollViewStateWithoutQuestionAndEnoughOptions) .finish() } @Test fun `given there is a question when enough options are added then poll can be created and more options can be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } - //delay(10) - createPollViewModel.test() - .assertState(pollViewStateWithQuestionAndEnoughOptions) + test + .assertLatestState(pollViewStateWithQuestionAndEnoughOptions) .finish() } @Test fun `given there is a question when max number of options are added then poll can be created and more options cannot be added`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) repeat(CreatePollViewModel.MAX_OPTIONS_COUNT) { if (it >= CreatePollViewModel.MIN_OPTIONS_COUNT) { @@ -152,20 +155,26 @@ class CreatePollViewModelTest { createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it])) } - delay(10) - createPollViewModel - .test() - .assertState(pollViewStateWithQuestionAndMaxOptions) + test + .assertLatestState(pollViewStateWithQuestionAndMaxOptions) .finish() } @Test fun `given an initial poll state when poll type is changed then view state is updated accordingly`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) - createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.DISCLOSED)) - createPollViewModel.awaitState().pollType shouldBe PollType.DISCLOSED + val test = createPollViewModel.test() + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.UNDISCLOSED)) - createPollViewModel.awaitState().pollType shouldBe PollType.UNDISCLOSED + createPollViewModel.handle(CreatePollAction.OnPollTypeChanged(PollType.DISCLOSED)) + + test + .assertStatesChanges( + initialCreatePollViewState, + { copy(pollType = PollType.UNDISCLOSED) }, + { copy(pollType = PollType.DISCLOSED) }, + ) + .finish() } @Test @@ -185,7 +194,7 @@ class CreatePollViewModelTest { .assertEvents( CreatePollViewEvents.EmptyQuestionError, CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT), - CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT) + CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = CreatePollViewModel.MIN_OPTIONS_COUNT), ) } @@ -201,29 +210,31 @@ class CreatePollViewModelTest { test .assertEvents( - CreatePollViewEvents.Success + CreatePollViewEvents.Success, ) } @Test fun `given there is a question and enough options when the last option is deleted then view state should be updated accordingly`() = runTest { val createPollViewModel = createPollViewModel(PollMode.CREATE) + val test = createPollViewModel.test() createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION)) createPollViewModel.handle(CreatePollAction.OnOptionChanged(0, A_FAKE_OPTIONS[0])) createPollViewModel.handle(CreatePollAction.OnOptionChanged(1, A_FAKE_OPTIONS[1])) createPollViewModel.handle(CreatePollAction.OnDeleteOption(1)) - delay(10) - createPollViewModel.test().assertState(pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption) + test.assertLatestState(pollViewStateWithQuestionAndEnoughOptionsButDeletedLastOption) } @Test fun `given an edited poll event when question and options are changed then view state is updated accordingly`() = runTest { val createPollViewModel = createPollViewModel(PollMode.EDIT) + val test = createPollViewModel.test() - delay(10) - createPollViewModel.test().assertState(editedPollViewState) + test + .assertState(editedPollViewState) + .finish() } @Test @@ -235,7 +246,7 @@ class CreatePollViewModelTest { test .assertEvents( - CreatePollViewEvents.Success + CreatePollViewEvents.Success, ) } } diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt index e5d5af2ece..5ac17cc5ff 100644 --- a/vector/src/test/java/im/vector/app/test/Extensions.kt +++ b/vector/src/test/java/im/vector/app/test/Extensions.kt @@ -86,6 +86,11 @@ class ViewModelTest( return this } + fun assertLatestState(expected: S): ViewModelTest { + states.assertLatestValue(expected) + return this + } + fun finish() { states.finish() viewEvents.finish() diff --git a/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt index 37f5f118c1..db828be232 100644 --- a/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt +++ b/vector/src/test/java/im/vector/app/test/FlowTestObserver.kt @@ -47,6 +47,10 @@ class FlowTestObserver( return this } + fun assertLatestValue(value: T) { + assertTrue(values.last() == value) + } + fun assertValues(values: List): FlowTestObserver { assertEquals(values, this.values) return this From a3774c11612ba32829be400b2b76cd948e8236ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jun 2022 23:08:07 +0000 Subject: [PATCH 136/314] Bump android-embedded_fcm_distributor from 2.0.0 to 2.1.1 Bumps [android-embedded_fcm_distributor](https://github.com/UnifiedPush/android-embedded_fcm_distributor) from 2.0.0 to 2.1.1. - [Release notes](https://github.com/UnifiedPush/android-embedded_fcm_distributor/releases) - [Commits](https://github.com/UnifiedPush/android-embedded_fcm_distributor/compare/2.0.0...2.1.1) --- updated-dependencies: - dependency-name: com.github.UnifiedPush:android-embedded_fcm_distributor dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 8d704141e5..735c953db7 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -465,7 +465,7 @@ dependencies { // UnifiedPush implementation 'com.github.UnifiedPush:android-connector:2.0.0' // UnifiedPush gplay flavor only - gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.0.0') { + gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From 3942e9bfa9c14bd50bfd5d271422358b5bd3af9d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 09:45:56 +0200 Subject: [PATCH 137/314] Add link to the Matrix room. --- docs/flipper.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/flipper.md b/docs/flipper.md index cc3b7eb5e0..495425ce5f 100644 --- a/docs/flipper.md +++ b/docs/flipper.md @@ -53,5 +53,6 @@ https://fbflipper.com/docs/getting-started/troubleshooting/android/ may help. ## Links -- https://fbflipper.com +- Official Flipper website: https://fbflipper.com - Realm Plugin for Flipper: https://github.com/kamgurgul/Flipper-Realm +- Dedicated Matrix room: https://matrix.to/#/#unifiedpush:matrix.org From 0011eda8e0adb3dc661213047de5c1cae0ca7b93 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 3 May 2022 15:41:31 +0200 Subject: [PATCH 138/314] Adding changelog entry --- changelog.d/5913.misc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/5913.misc diff --git a/changelog.d/5913.misc b/changelog.d/5913.misc new file mode 100644 index 0000000000..b0f562f069 --- /dev/null +++ b/changelog.d/5913.misc @@ -0,0 +1,2 @@ +- Notify of the latest known location in LocationTracker to avoid multiple locations at start +- Debounce location updates From 7860173fa22e7480353ef3dedc04903af5eef0d9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 3 May 2022 16:35:53 +0200 Subject: [PATCH 139/314] Notify of the latest known location among all last known locations --- .../app/features/location/LocationTracker.kt | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 1913f4202d..6f9d4b7870 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -57,34 +57,36 @@ class LocationTracker @Inject constructor( return } - locationManager.allProviders - .takeIf { it.isNotEmpty() } - // Take GPS first - ?.sortedByDescending { if (it == LocationManager.GPS_PROVIDER) 1 else 0 } - ?.forEach { provider -> - Timber.d("## LocationTracker. track location using $provider") + val providers = locationManager.allProviders - // Send last known location without waiting location updates - locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation -> - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## LocationTracker. lastKnownLocation: $lastKnownLocation") - } else { - Timber.d("## LocationTracker. lastKnownLocation: ${lastKnownLocation.provider}") - } - notifyLocation(lastKnownLocation, isLive = false) + if (providers.isEmpty()) { + callbacks.forEach { it.onLocationProviderIsNotAvailable() } + Timber.v("## LocationTracker. There is no location provider available") + } else { + // Take GPS first + providers.sortedByDescending { if (it == LocationManager.GPS_PROVIDER) 1 else 0 } + .mapNotNull { provider -> + Timber.d("## LocationTracker. track location using $provider") + + locationManager.requestLocationUpdates( + provider, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + this + ) + + locationManager.getLastKnownLocation(provider) } - - locationManager.requestLocationUpdates( - provider, - MIN_TIME_TO_UPDATE_LOCATION_MILLIS, - MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, - this - ) - } - ?: run { - callbacks.forEach { it.onLocationProviderIsNotAvailable() } - Timber.v("## LocationTracker. There is no location provider available") - } + .maxByOrNull { location -> location.time } + ?.let { latestKnownLocation -> + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("## LocationTracker. lastKnownLocation: $latestKnownLocation") + } else { + Timber.d("## LocationTracker. lastKnownLocation: ${latestKnownLocation.provider}") + } + notifyLocation(latestKnownLocation, isLive = false) + } + } } @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) @@ -125,6 +127,7 @@ class LocationTracker @Inject constructor( } private fun notifyLocation(location: Location, isLive: Boolean) { + Timber.d("## LocationTracker. notify location") when (location.provider) { LocationManager.GPS_PROVIDER -> { hasGpsProviderLiveLocation = isLive From c61412520dff34e746ea8f5562164ffea3f99d0e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 3 May 2022 16:55:14 +0200 Subject: [PATCH 140/314] Debouncing location updates --- .../app/features/location/LocationTracker.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 6f9d4b7870..2a5dff57d5 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -44,6 +44,8 @@ class LocationTracker @Inject constructor( private var hasGpsProviderLiveLocation = false + private var lastLocationUpdateMillis: Long? = null + private var lastLocation: LocationData? = null @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) @@ -84,7 +86,7 @@ class LocationTracker @Inject constructor( } else { Timber.d("## LocationTracker. lastKnownLocation: ${latestKnownLocation.provider}") } - notifyLocation(latestKnownLocation, isLive = false) + notifyLocation(latestKnownLocation) } } } @@ -123,14 +125,10 @@ class LocationTracker @Inject constructor( } else { Timber.d("## LocationTracker. onLocationChanged: ${location.provider}") } - notifyLocation(location, isLive = true) - } - private fun notifyLocation(location: Location, isLive: Boolean) { - Timber.d("## LocationTracker. notify location") when (location.provider) { LocationManager.GPS_PROVIDER -> { - hasGpsProviderLiveLocation = isLive + hasGpsProviderLiveLocation = true } else -> { if (hasGpsProviderLiveLocation) { @@ -140,9 +138,22 @@ class LocationTracker @Inject constructor( } } } + + val nowMillis = System.currentTimeMillis() + val elapsedMillis = nowMillis - (lastLocationUpdateMillis ?: 0) + if (elapsedMillis >= MIN_TIME_TO_UPDATE_LOCATION_MILLIS) { + lastLocationUpdateMillis = nowMillis + notifyLocation(location) + } else { + Timber.d("## LocationTracker. ignoring location: update is too fast") + } + } + + private fun notifyLocation(location: Location) { + Timber.d("## LocationTracker. notify location") val locationData = location.toLocationData() lastLocation = locationData - callbacks.forEach { it.onLocationUpdate(locationData) } + callbacks.forEach { it.onLocationUpdate(location.toLocationData()) } } override fun onProviderDisabled(provider: String) { From 5e6422b64c95ab7c3e350851f3c8020b783e7a9b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 3 Jun 2022 09:52:13 +0200 Subject: [PATCH 141/314] Updating changelog --- changelog.d/5913.misc | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/5913.misc b/changelog.d/5913.misc index b0f562f069..24be7194d5 100644 --- a/changelog.d/5913.misc +++ b/changelog.d/5913.misc @@ -1,2 +1,3 @@ - Notify of the latest known location in LocationTracker to avoid multiple locations at start - Debounce location updates +- Improve location providers access From a8c36e5e70534f80b2315a7de92587061583ce4d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 3 Jun 2022 11:14:34 +0200 Subject: [PATCH 142/314] Using Debouncer to debounce location updates --- .../app/features/location/LocationTracker.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 2a5dff57d5..66a0cac4bd 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -24,10 +24,15 @@ import androidx.annotation.RequiresPermission import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat import im.vector.app.BuildConfig +import im.vector.app.core.utils.Debouncer +import im.vector.app.core.utils.createBackgroundHandler import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +private const val BKG_HANDLER_NAME = "LocationTracker.BKG_HANDLER_NAME" +private const val LOCATION_DEBOUNCE_ID = "LocationTracker.LOCATION_DEBOUNCE_ID" + @Singleton class LocationTracker @Inject constructor( context: Context @@ -44,14 +49,13 @@ class LocationTracker @Inject constructor( private var hasGpsProviderLiveLocation = false - private var lastLocationUpdateMillis: Long? = null - private var lastLocation: LocationData? = null + private val debouncer = Debouncer(createBackgroundHandler(BKG_HANDLER_NAME)) + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { Timber.d("## LocationTracker. start()") - hasGpsProviderLiveLocation = false if (locationManager == null) { callbacks.forEach { it.onLocationProviderIsNotAvailable() } @@ -96,6 +100,8 @@ class LocationTracker @Inject constructor( Timber.d("## LocationTracker. stop()") locationManager?.removeUpdates(this) callbacks.clear() + debouncer.cancelAll() + hasGpsProviderLiveLocation = false } /** @@ -139,18 +145,18 @@ class LocationTracker @Inject constructor( } } - val nowMillis = System.currentTimeMillis() - val elapsedMillis = nowMillis - (lastLocationUpdateMillis ?: 0) - if (elapsedMillis >= MIN_TIME_TO_UPDATE_LOCATION_MILLIS) { - lastLocationUpdateMillis = nowMillis + debouncer.debounce(LOCATION_DEBOUNCE_ID, MIN_TIME_TO_UPDATE_LOCATION_MILLIS) { notifyLocation(location) - } else { - Timber.d("## LocationTracker. ignoring location: update is too fast") } } private fun notifyLocation(location: Location) { - Timber.d("## LocationTracker. notify location") + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("## LocationTracker. notify location: $location") + } else { + Timber.d("## LocationTracker. notify location: ${location.provider}") + } + val locationData = location.toLocationData() lastLocation = locationData callbacks.forEach { it.onLocationUpdate(location.toLocationData()) } From b686d30b1ceae3ff0c3e19a1dbbf237dfd3dfce7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 3 Jun 2022 11:53:59 +0200 Subject: [PATCH 143/314] Prioritise providers: Fused, GPS and then others --- .../app/features/location/LocationTracker.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 66a0cac4bd..84836e53c0 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -47,7 +47,8 @@ class LocationTracker @Inject constructor( private val callbacks = mutableListOf() - private var hasGpsProviderLiveLocation = false + private var hasLocationFromFusedProvider = false + private var hasLocationFromGPSProvider = false private var lastLocation: LocationData? = null @@ -70,7 +71,7 @@ class LocationTracker @Inject constructor( Timber.v("## LocationTracker. There is no location provider available") } else { // Take GPS first - providers.sortedByDescending { if (it == LocationManager.GPS_PROVIDER) 1 else 0 } + providers.sortedByDescending { getProviderPriority(it) } .mapNotNull { provider -> Timber.d("## LocationTracker. track location using $provider") @@ -95,13 +96,24 @@ class LocationTracker @Inject constructor( } } + /** + * Compute the priority of the given provider name. + * @return an integer representing the priority: the higher the value, the higher the priority is + */ + private fun getProviderPriority(provider: String): Int = when (provider) { + LocationManager.FUSED_PROVIDER -> 2 + LocationManager.GPS_PROVIDER -> 1 + else -> 0 + } + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun stop() { Timber.d("## LocationTracker. stop()") locationManager?.removeUpdates(this) callbacks.clear() debouncer.cancelAll() - hasGpsProviderLiveLocation = false + hasLocationFromGPSProvider = false + hasLocationFromFusedProvider = false } /** @@ -133,13 +145,23 @@ class LocationTracker @Inject constructor( } when (location.provider) { + LocationManager.FUSED_PROVIDER -> { + hasLocationFromFusedProvider = true + } LocationManager.GPS_PROVIDER -> { - hasGpsProviderLiveLocation = true + if (hasLocationFromFusedProvider) { + hasLocationFromGPSProvider = false + // Ignore this update + Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have location from fused provider") + return + } else { + hasLocationFromGPSProvider = true + } } else -> { - if (hasGpsProviderLiveLocation) { + if (hasLocationFromFusedProvider || hasLocationFromGPSProvider) { // Ignore this update - Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps live location") + Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have location from GPS provider") return } } @@ -164,6 +186,10 @@ class LocationTracker @Inject constructor( override fun onProviderDisabled(provider: String) { Timber.d("## LocationTracker. onProviderDisabled: $provider") + when (provider) { + LocationManager.FUSED_PROVIDER -> hasLocationFromFusedProvider = false + LocationManager.GPS_PROVIDER -> hasLocationFromGPSProvider = false + } callbacks.forEach { it.onLocationProviderIsNotAvailable() } } From 45d3fe7c071f2e117bde851b662f3b158d00a2b0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 3 Jun 2022 14:11:52 +0200 Subject: [PATCH 144/314] Call no provider available callback only providers list is empty --- .../location/LocationSharingService.kt | 2 +- .../location/LocationSharingViewModel.kt | 2 +- .../app/features/location/LocationTracker.kt | 50 ++++++++++++------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 8b9a1c75ae..62aba9318c 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -198,7 +198,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { ) } - override fun onLocationProviderIsNotAvailable() { + override fun onNoLocationProviderAvailable() { stopForeground(true) stopSelf() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 9ca340f098..71f59c6fdf 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -182,7 +182,7 @@ class LocationSharingViewModel @AssistedInject constructor( } } - override fun onLocationProviderIsNotAvailable() { + override fun onNoLocationProviderAvailable() { _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 84836e53c0..c14ec214f0 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -41,8 +41,15 @@ class LocationTracker @Inject constructor( private val locationManager = context.getSystemService() interface Callback { + /** + * Called on every location update. + */ fun onLocationUpdate(locationData: LocationData) - fun onLocationProviderIsNotAvailable() + + /** + * Called when no location provider is available to request location updates. + */ + fun onNoLocationProviderAvailable() } private val callbacks = mutableListOf() @@ -56,24 +63,24 @@ class LocationTracker @Inject constructor( @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { - Timber.d("## LocationTracker. start()") + Timber.d("start()") if (locationManager == null) { - callbacks.forEach { it.onLocationProviderIsNotAvailable() } - Timber.v("## LocationTracker. LocationManager is not available") + callbacks.forEach { it.onNoLocationProviderAvailable() } + Timber.v("LocationManager is not available") return } val providers = locationManager.allProviders if (providers.isEmpty()) { - callbacks.forEach { it.onLocationProviderIsNotAvailable() } - Timber.v("## LocationTracker. There is no location provider available") + callbacks.forEach { it.onNoLocationProviderAvailable() } + Timber.v("There is no location provider available") } else { // Take GPS first providers.sortedByDescending { getProviderPriority(it) } .mapNotNull { provider -> - Timber.d("## LocationTracker. track location using $provider") + Timber.d("track location using $provider") locationManager.requestLocationUpdates( provider, @@ -87,9 +94,9 @@ class LocationTracker @Inject constructor( .maxByOrNull { location -> location.time } ?.let { latestKnownLocation -> if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## LocationTracker. lastKnownLocation: $latestKnownLocation") + Timber.d("lastKnownLocation: $latestKnownLocation") } else { - Timber.d("## LocationTracker. lastKnownLocation: ${latestKnownLocation.provider}") + Timber.d("lastKnownLocation: ${latestKnownLocation.provider}") } notifyLocation(latestKnownLocation) } @@ -98,7 +105,7 @@ class LocationTracker @Inject constructor( /** * Compute the priority of the given provider name. - * @return an integer representing the priority: the higher the value, the higher the priority is + * @return an integer representing the priority: the higher the value, the higher the priority is. */ private fun getProviderPriority(provider: String): Int = when (provider) { LocationManager.FUSED_PROVIDER -> 2 @@ -108,7 +115,7 @@ class LocationTracker @Inject constructor( @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun stop() { - Timber.d("## LocationTracker. stop()") + Timber.d("stop()") locationManager?.removeUpdates(this) callbacks.clear() debouncer.cancelAll() @@ -139,9 +146,9 @@ class LocationTracker @Inject constructor( override fun onLocationChanged(location: Location) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## LocationTracker. onLocationChanged: $location") + Timber.d("onLocationChanged: $location") } else { - Timber.d("## LocationTracker. onLocationChanged: ${location.provider}") + Timber.d("onLocationChanged: ${location.provider}") } when (location.provider) { @@ -152,7 +159,7 @@ class LocationTracker @Inject constructor( if (hasLocationFromFusedProvider) { hasLocationFromGPSProvider = false // Ignore this update - Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have location from fused provider") + Timber.d("ignoring location from ${location.provider}, we have location from fused provider") return } else { hasLocationFromGPSProvider = true @@ -161,7 +168,7 @@ class LocationTracker @Inject constructor( else -> { if (hasLocationFromFusedProvider || hasLocationFromGPSProvider) { // Ignore this update - Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have location from GPS provider") + Timber.d("ignoring location from ${location.provider}, we have location from GPS provider") return } } @@ -174,9 +181,9 @@ class LocationTracker @Inject constructor( private fun notifyLocation(location: Location) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## LocationTracker. notify location: $location") + Timber.d("notify location: $location") } else { - Timber.d("## LocationTracker. notify location: ${location.provider}") + Timber.d("notify location: ${location.provider}") } val locationData = location.toLocationData() @@ -185,12 +192,17 @@ class LocationTracker @Inject constructor( } override fun onProviderDisabled(provider: String) { - Timber.d("## LocationTracker. onProviderDisabled: $provider") + Timber.d("onProviderDisabled: $provider") when (provider) { LocationManager.FUSED_PROVIDER -> hasLocationFromFusedProvider = false LocationManager.GPS_PROVIDER -> hasLocationFromGPSProvider = false } - callbacks.forEach { it.onLocationProviderIsNotAvailable() } + + locationManager?.allProviders + ?.takeIf { it.isEmpty() } + ?.let { + callbacks.forEach { it.onNoLocationProviderAvailable() } + } } private fun Location.toLocationData(): LocationData { From 260f73b0c247e1e1207be88e5094b7ca01f25a4d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 7 Jun 2022 17:09:41 +0200 Subject: [PATCH 145/314] Adding unit tests for LocationTracker --- .../location/LocationSharingService.kt | 1 + .../location/LocationSharingViewModel.kt | 1 + .../app/features/location/LocationTracker.kt | 14 +- .../features/location/LocationTrackerTest.kt | 294 ++++++++++++++++++ .../im/vector/app/test/fakes/FakeContext.kt | 2 +- .../im/vector/app/test/fakes/FakeHandler.kt | 25 ++ .../app/test/fakes/FakeLocationManager.kt | 48 +++ 7 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeLocationManager.kt diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 62aba9318c..73a86fd04e 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -205,6 +205,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { private fun destroyMe() { locationTracker.removeCallback(this) + locationTracker.stop() timers.forEach { it.cancel() } timers.clear() stopSelf() diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 71f59c6fdf..f049a9400a 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -114,6 +114,7 @@ class LocationSharingViewModel @AssistedInject constructor( override fun onCleared() { super.onCleared() locationTracker.removeCallback(this) + locationTracker.stop() } override fun handle(action: LocationSharingAction) { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index c14ec214f0..3e0d60f2c8 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -21,6 +21,7 @@ import android.content.Context import android.location.Location import android.location.LocationManager import androidx.annotation.RequiresPermission +import androidx.annotation.VisibleForTesting import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat import im.vector.app.BuildConfig @@ -52,10 +53,14 @@ class LocationTracker @Inject constructor( fun onNoLocationProviderAvailable() } - private val callbacks = mutableListOf() + @VisibleForTesting + val callbacks = mutableListOf() - private var hasLocationFromFusedProvider = false - private var hasLocationFromGPSProvider = false + @VisibleForTesting + var hasLocationFromFusedProvider = false + + @VisibleForTesting + var hasLocationFromGPSProvider = false private var lastLocation: LocationData? = null @@ -139,9 +144,6 @@ class LocationTracker @Inject constructor( fun removeCallback(callback: Callback) { callbacks.remove(callback) - if (callbacks.size == 0) { - stop() - } } override fun onLocationChanged(location: Location) { diff --git a/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt new file mode 100644 index 0000000000..409647a813 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt @@ -0,0 +1,294 @@ +/* + * 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.location + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import im.vector.app.core.utils.Debouncer +import im.vector.app.core.utils.createBackgroundHandler +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeHandler +import im.vector.app.test.fakes.FakeLocationManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.slot +import io.mockk.unmockkConstructor +import io.mockk.unmockkStatic +import io.mockk.verify +import io.mockk.verifyOrder +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test + +private const val A_LATITUDE = 1.2 +private const val A_LONGITUDE = 44.0 +private const val AN_ACCURACY = 5.0f + +class LocationTrackerTest { + + private val fakeHandler = FakeHandler() + + init { + mockkConstructor(Debouncer::class) + every { anyConstructed().cancelAll() } just runs + val runnable = slot() + every { anyConstructed().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, capture(runnable)) } answers { + runnable.captured.run() + true + } + mockkStatic("im.vector.app.core.utils.HandlerKt") + every { createBackgroundHandler(any()) } returns fakeHandler.instance + } + + private val fakeLocationManager = FakeLocationManager() + private val fakeContext = FakeContext().also { + it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance) + } + + private val locationTracker = LocationTracker(fakeContext.instance) + + @Before + fun setUp() { + fakeLocationManager.givenRemoveUpdates(locationTracker) + } + + @After + fun tearDown() { + unmockkStatic("im.vector.app.core.utils.HandlerKt") + unmockkConstructor(Debouncer::class) + } + + @Test + fun `given available list of providers when starting then location updates are requested in priority order`() { + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) + mockAvailableProviders(providers) + + locationTracker.start() + + verifyOrder { + fakeLocationManager.instance.requestLocationUpdates( + LocationManager.FUSED_PROVIDER, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + locationTracker + ) + fakeLocationManager.instance.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + locationTracker + ) + fakeLocationManager.instance.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + locationTracker + ) + } + } + + @Test + fun `given available list of providers when list is empty then callbacks are notified`() { + val providers = emptyList() + val callback = mockCallback() + + locationTracker.addCallback(callback) + fakeLocationManager.givenActiveProviders(providers) + + locationTracker.start() + + verify { callback.onNoLocationProviderAvailable() } + locationTracker.removeCallback(callback) + } + + @Test + fun `when adding or removing a callback then it is added into or removed from the list of callbacks`() { + val callback = mockCallback() + + locationTracker.addCallback(callback) + + locationTracker.callbacks.size shouldBeEqualTo 1 + locationTracker.callbacks.first() shouldBeEqualTo callback + + locationTracker.removeCallback(callback) + + locationTracker.callbacks.size shouldBeEqualTo 0 + } + + @Test + fun `when location updates are received from fused provider then fused locations are taken in priority`() { + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) + mockAvailableProviders(providers) + val callback = mockCallback() + locationTracker.addCallback(callback) + locationTracker.start() + + val fusedLocation = mockLocation( + provider = LocationManager.FUSED_PROVIDER, + latitude = 1.0, + longitude = 3.0, + accuracy = 4f + ) + val gpsLocation = mockLocation( + provider = LocationManager.GPS_PROVIDER + ) + + val networkLocation = mockLocation( + provider = LocationManager.NETWORK_PROVIDER + ) + locationTracker.onLocationChanged(fusedLocation) + locationTracker.onLocationChanged(gpsLocation) + locationTracker.onLocationChanged(networkLocation) + + val expectedLocationData = LocationData( + latitude = 1.0, + longitude = 3.0, + uncertainty = 4.0 + ) + verify { callback.onLocationUpdate(expectedLocationData) } + verify { anyConstructed().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } + locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true + locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false + } + + @Test + fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() { + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) + mockAvailableProviders(providers) + val callback = mockCallback() + locationTracker.addCallback(callback) + locationTracker.start() + + val gpsLocation = mockLocation( + provider = LocationManager.GPS_PROVIDER, + latitude = 1.0, + longitude = 3.0, + accuracy = 4f + ) + + val networkLocation = mockLocation( + provider = LocationManager.NETWORK_PROVIDER + ) + locationTracker.onLocationChanged(gpsLocation) + locationTracker.onLocationChanged(networkLocation) + + val expectedLocationData = LocationData( + latitude = 1.0, + longitude = 3.0, + uncertainty = 4.0 + ) + verify { callback.onLocationUpdate(expectedLocationData) } + verify { anyConstructed().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } + locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false + locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true + } + + @Test + fun `when location updates are received from network provider then network locations are taken if none are received from fused or gps provider`() { + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) + mockAvailableProviders(providers) + val callback = mockCallback() + locationTracker.addCallback(callback) + locationTracker.start() + + val networkLocation = mockLocation( + provider = LocationManager.NETWORK_PROVIDER, + latitude = 1.0, + longitude = 3.0, + accuracy = 4f + ) + locationTracker.onLocationChanged(networkLocation) + + val expectedLocationData = LocationData( + latitude = 1.0, + longitude = 3.0, + uncertainty = 4.0 + ) + verify { callback.onLocationUpdate(expectedLocationData) } + verify { anyConstructed().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } + locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false + locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false + } + + @Test + fun `when requesting the last location then last location is notified via callback`() { + val providers = listOf(LocationManager.GPS_PROVIDER) + fakeLocationManager.givenActiveProviders(providers) + val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER) + fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation) + fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker) + val callback = mockCallback() + locationTracker.addCallback(callback) + locationTracker.start() + + locationTracker.requestLastKnownLocation() + + val expectedLocationData = LocationData( + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = AN_ACCURACY.toDouble() + ) + verify { callback.onLocationUpdate(expectedLocationData) } + } + + @Test + fun `when stopping then location updates are stopped and callbacks are cleared`() { + locationTracker.stop() + + verify { fakeLocationManager.instance.removeUpdates(locationTracker) } + verify { anyConstructed().cancelAll() } + locationTracker.callbacks.isEmpty() shouldBeEqualTo true + locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false + locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false + } + + private fun mockAvailableProviders(providers: List) { + fakeLocationManager.givenActiveProviders(providers) + providers.forEach { provider -> + fakeLocationManager.givenLastLocationForProvider(provider = provider, location = null) + fakeLocationManager.givenRequestUpdatesForProvider(provider = provider, listener = locationTracker) + } + } + + private fun mockCallback(): LocationTracker.Callback { + return mockk().also { + every { it.onNoLocationProviderAvailable() } just runs + every { it.onLocationUpdate(any()) } just runs + } + } + + private fun mockLocation( + provider: String, + latitude: Double = A_LATITUDE, + longitude: Double = A_LONGITUDE, + accuracy: Float = AN_ACCURACY + ): Location { + return mockk().also { + every { it.time } returns 123 + every { it.latitude } returns latitude + every { it.longitude } returns longitude + every { it.accuracy } returns accuracy + every { it.provider } returns provider + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt index eb491c9e0c..226f6458de 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt @@ -56,7 +56,7 @@ class FakeContext( givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) } - private fun givenService(name: String, klass: Class, service: T) { + fun givenService(name: String, klass: Class, service: T) { every { instance.getSystemService(name) } returns service every { instance.getSystemService(klass) } returns service } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt new file mode 100644 index 0000000000..11340946ec --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt @@ -0,0 +1,25 @@ +/* + * 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 android.os.Handler +import io.mockk.mockk + +class FakeHandler { + + val instance = mockk() +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationManager.kt new file mode 100644 index 0000000000..30c30e6b4a --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationManager.kt @@ -0,0 +1,48 @@ +/* + * 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 android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs + +class FakeLocationManager { + val instance = mockk() + + fun givenActiveProviders(providers: List) { + every { instance.allProviders } returns providers + } + + fun givenRequestUpdatesForProvider( + provider: String, + listener: LocationListener + ) { + every { instance.requestLocationUpdates(provider, any(), any(), listener) } just runs + } + + fun givenRemoveUpdates(listener: LocationListener) { + every { instance.removeUpdates(listener) } just runs + } + + fun givenLastLocationForProvider(provider: String, location: Location?) { + every { instance.getLastKnownLocation(provider) } returns location + } +} From 755d743b063d0773026b68ebe1846d9d10bc1035 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 09:54:49 +0200 Subject: [PATCH 146/314] Encapsulate callbacks calls into try/catch block --- .../app/features/location/LocationTracker.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 3e0d60f2c8..66005e71c6 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -71,16 +71,16 @@ class LocationTracker @Inject constructor( Timber.d("start()") if (locationManager == null) { - callbacks.forEach { it.onNoLocationProviderAvailable() } Timber.v("LocationManager is not available") + onNoLocationProviderAvailable() return } val providers = locationManager.allProviders if (providers.isEmpty()) { - callbacks.forEach { it.onNoLocationProviderAvailable() } Timber.v("There is no location provider available") + onNoLocationProviderAvailable() } else { // Take GPS first providers.sortedByDescending { getProviderPriority(it) } @@ -133,7 +133,7 @@ class LocationTracker @Inject constructor( * Please ensure adding a callback to receive the value. */ fun requestLastKnownLocation() { - lastLocation?.let { location -> callbacks.forEach { it.onLocationUpdate(location) } } + lastLocation?.let { locationData -> onLocationUpdate(locationData) } } fun addCallback(callback: Callback) { @@ -190,7 +190,7 @@ class LocationTracker @Inject constructor( val locationData = location.toLocationData() lastLocation = locationData - callbacks.forEach { it.onLocationUpdate(location.toLocationData()) } + onLocationUpdate(locationData) } override fun onProviderDisabled(provider: String) { @@ -203,10 +203,31 @@ class LocationTracker @Inject constructor( locationManager?.allProviders ?.takeIf { it.isEmpty() } ?.let { - callbacks.forEach { it.onNoLocationProviderAvailable() } + Timber.e("all providers have been disabled") + onNoLocationProviderAvailable() } } + private fun onNoLocationProviderAvailable() { + callbacks.forEach { + try { + it.onNoLocationProviderAvailable() + } catch (error: Exception) { + Timber.e(error, "error in onNoLocationProviderAvailable callback $it") + } + } + } + + private fun onLocationUpdate(locationData: LocationData) { + callbacks.forEach { + try { + it.onLocationUpdate(locationData) + } catch (error: Exception) { + Timber.e(error, "error in onLocationUpdate callback $it") + } + } + } + private fun Location.toLocationData(): LocationData { return LocationData(latitude, longitude, accuracy.toDouble()) } From 1b88cc39a93da5594f21b8573685e90dd200f321 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 09:55:27 +0200 Subject: [PATCH 147/314] Use method reference when sorting providers --- .../java/im/vector/app/features/location/LocationTracker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 66005e71c6..c900fc7db5 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -83,7 +83,7 @@ class LocationTracker @Inject constructor( onNoLocationProviderAvailable() } else { // Take GPS first - providers.sortedByDescending { getProviderPriority(it) } + providers.sortedByDescending(::getProviderPriority) .mapNotNull { provider -> Timber.d("track location using $provider") From a1aa337edb771312a4503e7eee331e75327a5a08 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 10:15:57 +0200 Subject: [PATCH 148/314] Stop location tracking only when callbacks list is empty to avoid non wanting stop --- .../features/location/LocationSharingService.kt | 1 - .../location/LocationSharingViewModel.kt | 1 - .../app/features/location/LocationTracker.kt | 3 +++ .../app/features/location/LocationTrackerTest.kt | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 73a86fd04e..62aba9318c 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -205,7 +205,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { private fun destroyMe() { locationTracker.removeCallback(this) - locationTracker.stop() timers.forEach { it.cancel() } timers.clear() stopSelf() diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index f049a9400a..71f59c6fdf 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -114,7 +114,6 @@ class LocationSharingViewModel @AssistedInject constructor( override fun onCleared() { super.onCleared() locationTracker.removeCallback(this) - locationTracker.stop() } override fun handle(action: LocationSharingAction) { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index c900fc7db5..63508f30d7 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -144,6 +144,9 @@ class LocationTracker @Inject constructor( fun removeCallback(callback: Callback) { callbacks.remove(callback) + if (callbacks.size == 0) { + stop() + } } override fun onLocationChanged(location: Location) { diff --git a/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt index 409647a813..0a04644856 100644 --- a/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt @@ -123,16 +123,24 @@ class LocationTrackerTest { @Test fun `when adding or removing a callback then it is added into or removed from the list of callbacks`() { - val callback = mockCallback() + val callback1 = mockCallback() + val callback2 = mockCallback() - locationTracker.addCallback(callback) + locationTracker.addCallback(callback1) + locationTracker.addCallback(callback2) + + locationTracker.callbacks.size shouldBeEqualTo 2 + locationTracker.callbacks.first() shouldBeEqualTo callback1 + locationTracker.callbacks[1] shouldBeEqualTo callback2 + + locationTracker.removeCallback(callback1) locationTracker.callbacks.size shouldBeEqualTo 1 - locationTracker.callbacks.first() shouldBeEqualTo callback - locationTracker.removeCallback(callback) + locationTracker.removeCallback(callback2) locationTracker.callbacks.size shouldBeEqualTo 0 + verify { locationTracker.stop() } } @Test From 162fd0a8403d60b8630967e9c99753312449fc8e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 11:35:44 +0200 Subject: [PATCH 149/314] Call unmockAll after each test --- .../features/location/LocationTrackerTest.kt | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt index 0a04644856..b0f3974226 100644 --- a/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/LocationTrackerTest.kt @@ -31,8 +31,7 @@ import io.mockk.mockkConstructor import io.mockk.mockkStatic import io.mockk.runs import io.mockk.slot -import io.mockk.unmockkConstructor -import io.mockk.unmockkStatic +import io.mockk.unmockkAll import io.mockk.verify import io.mockk.verifyOrder import org.amshove.kluent.shouldBeEqualTo @@ -47,8 +46,15 @@ private const val AN_ACCURACY = 5.0f class LocationTrackerTest { private val fakeHandler = FakeHandler() + private val fakeLocationManager = FakeLocationManager() + private val fakeContext = FakeContext().also { + it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance) + } - init { + private lateinit var locationTracker: LocationTracker + + @Before + fun setUp() { mockkConstructor(Debouncer::class) every { anyConstructed().cancelAll() } just runs val runnable = slot() @@ -58,24 +64,13 @@ class LocationTrackerTest { } mockkStatic("im.vector.app.core.utils.HandlerKt") every { createBackgroundHandler(any()) } returns fakeHandler.instance - } - - private val fakeLocationManager = FakeLocationManager() - private val fakeContext = FakeContext().also { - it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance) - } - - private val locationTracker = LocationTracker(fakeContext.instance) - - @Before - fun setUp() { + locationTracker = LocationTracker(fakeContext.instance) fakeLocationManager.givenRemoveUpdates(locationTracker) } @After fun tearDown() { - unmockkStatic("im.vector.app.core.utils.HandlerKt") - unmockkConstructor(Debouncer::class) + unmockkAll() } @Test From ba8fcf9de3054a9b0612e07e8ae621d9d8b3736e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 12:17:09 +0200 Subject: [PATCH 150/314] Suppress issue on a tool. --- tools/dependencycheck/suppressions.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/dependencycheck/suppressions.xml b/tools/dependencycheck/suppressions.xml index 758b1a87f3..932bcdc08d 100644 --- a/tools/dependencycheck/suppressions.xml +++ b/tools/dependencycheck/suppressions.xml @@ -14,4 +14,11 @@ ^pkg:maven/com\.pinterest\.ktlint/ktlint\-reporter\-checkstyle@.*$ CVE-2019-9658 + + + ^pkg:maven/io\.github\.detekt\.sarif4k/sarif4k@.*$ + cpe:/a:detekt:detekt + From daa571957a447d7c1fda7f76165ba4768380d564 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 13:20:37 +0200 Subject: [PATCH 151/314] Replace task by much faster `dependencyCheckAnalyze` --- .github/workflows/quality.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 014139d0ba..d0797721e6 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -113,13 +113,13 @@ jobs: steps: - uses: actions/checkout@v3 - name: Dependency analysis - run: ./gradlew buildHealth $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES - name: Upload dependency analysis if: always() uses: actions/upload-artifact@v3 with: name: dependency-analysis - path: build/reports/dependency-analysis/build-health-report.txt + path: build/reports/dependency-check-report.html # Lint for main module android-lint: From 663812b90b1e4fd81f8ebfb9829847571c2fc257 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 14:36:07 +0200 Subject: [PATCH 152/314] Format file --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b767da14d7..f6a1906394 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Assemble GPlay unsigned apk - run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES --stacktrace + run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES --stacktrace - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v3 with: @@ -88,6 +88,6 @@ jobs: with: name: exodus.json path: | - exodus.json + exodus.json - name: Check for trackers run: "jq -e '.trackers == []' exodus.json > /dev/null || { echo '::error static analysis identified user tracking library' ; exit 1; }" From 705b55c57a09c3b643a72aa918d5f83b205bcf33 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 16:53:33 +0200 Subject: [PATCH 153/314] Use handy kotlin.time.Duration.Companion and remove default value. --- .../java/im/vector/app/core/utils/TemporaryStoreTest.kt | 3 ++- .../main/java/im/vector/app/core/utils/TemporaryStore.kt | 9 ++++----- .../home/room/detail/RoomDetailPendingActionStore.kt | 3 ++- .../java/im/vector/app/features/login/ReAuthHelper.kt | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt index 71a83f2e9b..303c1673dd 100644 --- a/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt @@ -19,13 +19,14 @@ package im.vector.app.core.utils import org.amshove.kluent.shouldBe import org.junit.Test import java.lang.Thread.sleep +import kotlin.time.Duration.Companion.milliseconds class TemporaryStoreTest { @Test fun testTemporaryStore() { // Keep the data 300 millis - val store = TemporaryStore(300) + val store = TemporaryStore(300.milliseconds) store.data = "test" store.data shouldBe "test" diff --git a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt index bd1e396126..809ba1173a 100644 --- a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt +++ b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt @@ -18,15 +18,14 @@ package im.vector.app.core.utils import java.util.Timer import java.util.TimerTask - -const val THREE_MINUTES = 3 * 60_000L +import kotlin.time.Duration /** * Store an object T for a specific period of time. * @param T type of the data to store - * @property delay delay to keep the data, in millis + * @property delay delay to keep the data */ -open class TemporaryStore(private val delay: Long = THREE_MINUTES) { +open class TemporaryStore(private val delay: Duration) { private var timer: Timer? = null @@ -40,7 +39,7 @@ open class TemporaryStore(private val delay: Long = THREE_MINUTES) { override fun run() { field = null } - }, delay) + }, delay.inWholeMilliseconds) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt index 9ffbb83a47..9dd6569cbd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt @@ -19,7 +19,8 @@ package im.vector.app.features.home.room.detail import im.vector.app.core.utils.TemporaryStore import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.seconds // Store to keep a pending action from sub screen of a room detail @Singleton -class RoomDetailPendingActionStore @Inject constructor() : TemporaryStore(10_000) +class RoomDetailPendingActionStore @Inject constructor() : TemporaryStore(10.seconds) diff --git a/vector/src/main/java/im/vector/app/features/login/ReAuthHelper.kt b/vector/src/main/java/im/vector/app/features/login/ReAuthHelper.kt index b29c930234..95cb4cc879 100644 --- a/vector/src/main/java/im/vector/app/features/login/ReAuthHelper.kt +++ b/vector/src/main/java/im/vector/app/features/login/ReAuthHelper.kt @@ -19,9 +19,10 @@ package im.vector.app.features.login import im.vector.app.core.utils.TemporaryStore import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.minutes /** * Will store the account password for 3 minutes. */ @Singleton -class ReAuthHelper @Inject constructor() : TemporaryStore() +class ReAuthHelper @Inject constructor() : TemporaryStore(3.minutes) From 706e8e76277d5b94cc646ee1cbef8b4630eac889 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 16:54:18 +0200 Subject: [PATCH 154/314] Ensure the test is less flaky... --- .../java/im/vector/app/core/utils/TemporaryStoreTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt index 303c1673dd..70af6e6419 100644 --- a/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TemporaryStoreTest.kt @@ -30,7 +30,7 @@ class TemporaryStoreTest { store.data = "test" store.data shouldBe "test" - sleep(10) + sleep(100) store.data shouldBe "test" sleep(300) store.data shouldBe null From dee5dfd187174ac9c156c1b672abb3ddcea3707d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 12:02:21 +0200 Subject: [PATCH 155/314] Add synchronized annotations to protect from concurrent access to callbacks --- .../im/vector/app/features/location/LocationTracker.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 63508f30d7..cdf13a7004 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -122,7 +122,9 @@ class LocationTracker @Inject constructor( fun stop() { Timber.d("stop()") locationManager?.removeUpdates(this) - callbacks.clear() + synchronized(this) { + callbacks.clear() + } debouncer.cancelAll() hasLocationFromGPSProvider = false hasLocationFromFusedProvider = false @@ -136,12 +138,14 @@ class LocationTracker @Inject constructor( lastLocation?.let { locationData -> onLocationUpdate(locationData) } } + @Synchronized fun addCallback(callback: Callback) { if (!callbacks.contains(callback)) { callbacks.add(callback) } } + @Synchronized fun removeCallback(callback: Callback) { callbacks.remove(callback) if (callbacks.size == 0) { @@ -211,6 +215,7 @@ class LocationTracker @Inject constructor( } } + @Synchronized private fun onNoLocationProviderAvailable() { callbacks.forEach { try { @@ -221,6 +226,7 @@ class LocationTracker @Inject constructor( } } + @Synchronized private fun onLocationUpdate(locationData: LocationData) { callbacks.forEach { try { From ba0898831b75a9482860a7b22e9c233d0ae151fb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Jun 2022 17:22:31 +0200 Subject: [PATCH 156/314] Fix compilation issue --- .../gplay/features/settings/troubleshoot/TestPlayServices.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt index f1ea4a4153..e78132908d 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt @@ -46,7 +46,7 @@ class TestPlayServices @Inject constructor( if (apiAvailability.isUserResolvableError(resultCode)) { quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_play_services_quickfix) { override fun doFix() { - apiAvailability.getErrorDialog(context, resultCode, 9000 /*hey does the magic number*/).show() + apiAvailability.getErrorDialog(context, resultCode, 9000 /*hey does the magic number*/)?.show() } } Timber.e("Play Services apk error $resultCode -> ${apiAvailability.getErrorString(resultCode)}.") From dc95f4553e18526e242569756f3fa970e5364eaf Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 8 Jun 2022 10:38:05 +0200 Subject: [PATCH 157/314] Adding changelog entry --- changelog.d/6155.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6155.misc diff --git a/changelog.d/6155.misc b/changelog.d/6155.misc new file mode 100644 index 0000000000..044e21408e --- /dev/null +++ b/changelog.d/6155.misc @@ -0,0 +1 @@ +Add unit tests for LiveLocationAggregationProcessor code From 51b930147aa3df3a8728808599bc645eceff2625 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 8 Jun 2022 11:30:08 +0200 Subject: [PATCH 158/314] Adding some tests on ignored cases --- .../LiveLocationAggregationProcessor.kt | 28 +++- .../LiveLocationAggregationProcessorTest.kt | 128 ++++++++++++++++++ .../android/sdk/test/fakes/FakeClock.kt | 27 ++++ .../sdk/test/fakes/FakeWorkManagerProvider.kt | 25 ++++ 4 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 05bde8f83f..a254552bb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -36,16 +36,22 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -// TODO add unit tests +/** + * Aggregates all live location sharing related events in local database. + */ internal class LiveLocationAggregationProcessor @Inject constructor( @SessionId private val sessionId: String, private val workManagerProvider: WorkManagerProvider, private val clock: Clock, ) { - fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) { + /** + * Handle the content of a beacon info. + * @return true if it has been processed, false if ignored. + */ + fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean): Boolean { if (event.senderId.isNullOrEmpty() || isLocalEcho) { - return + return false } val isLive = content.isLive.orTrue() @@ -58,7 +64,7 @@ internal class LiveLocationAggregationProcessor @Inject constructor( if (targetEventId.isNullOrEmpty()) { Timber.w("no target event id found for the beacon content") - return + return false } val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( @@ -83,6 +89,8 @@ internal class LiveLocationAggregationProcessor @Inject constructor( } else { cancelDeactivationAfterTimeout(targetEventId, roomId) } + + return true } private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) { @@ -110,6 +118,10 @@ internal class LiveLocationAggregationProcessor @Inject constructor( workManagerProvider.workManager.cancelUniqueWork(workName) } + /** + * Handle the content of a beacon location data. + * @return true if it has been processed, false if ignored. + */ fun handleBeaconLocationData( realm: Realm, event: Event, @@ -117,14 +129,14 @@ internal class LiveLocationAggregationProcessor @Inject constructor( roomId: String, relatedEventId: String?, isLocalEcho: Boolean - ) { + ): Boolean { if (event.senderId.isNullOrEmpty() || isLocalEcho) { - return + return false } if (relatedEventId.isNullOrEmpty()) { Timber.w("no related event id found for the live location content") - return + return false } val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( @@ -143,6 +155,8 @@ internal class LiveLocationAggregationProcessor @Inject constructor( Timber.d("updating last location of the summary of id=$relatedEventId") aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent()) } + + return true } private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt new file mode 100644 index 0000000000..d8c7f7477b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -0,0 +1,128 @@ +/* + * 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.internal.session.room.aggregation.livelocation + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider + +private const val A_SESSION_ID = "session_id" +private const val A_SENDER_ID = "sender_id" +private const val AN_EVENT_ID = "event_id" + +internal class LiveLocationAggregationProcessorTest { + + private val fakeWorkManagerProvider = FakeWorkManagerProvider() + private val fakeClock = FakeClock() + private val fakeRealm = FakeRealm() + + private val liveLocationAggregationProcessor = LiveLocationAggregationProcessor( + sessionId = A_SESSION_ID, + workManagerProvider = fakeWorkManagerProvider.instance, + clock = fakeClock + ) + + @Test + fun `given beacon info when it is local echo then it is ignored`() { + val event = Event(senderId = A_SENDER_ID) + val beaconInfo = MessageBeaconInfoContent() + + val result = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = event, + content = beaconInfo, + roomId = "", + isLocalEcho = true + ) + + result shouldBeEqualTo false + } + + @Test + fun `given beacon info and event when senderId is null or empty then it is ignored`() { + val eventNoSenderId = Event(eventId = AN_EVENT_ID) + val eventEmptySenderId = Event(eventId = AN_EVENT_ID, senderId = "") + val beaconInfo = MessageBeaconInfoContent() + + val resultNoSenderId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventNoSenderId, + content = beaconInfo, + roomId = "", + isLocalEcho = false + ) + val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventEmptySenderId, + content = beaconInfo, + roomId = "", + isLocalEcho = false + ) + + resultNoSenderId shouldBeEqualTo false + resultEmptySenderId shouldBeEqualTo false + } + + @Test + fun `given beacon location data when it is local echo then it is ignored`() { + val event = Event(senderId = A_SENDER_ID) + val beaconLocationData = MessageBeaconLocationDataContent() + + val result = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = "", + relatedEventId = "", + isLocalEcho = true + ) + + result shouldBeEqualTo false + } + + @Test + fun `given beacon location data and event when senderId is null or empty then it is ignored`() { + val eventNoSenderId = Event(eventId = AN_EVENT_ID) + val eventEmptySenderId = Event(eventId = AN_EVENT_ID, senderId = "") + val beaconLocationData = MessageBeaconLocationDataContent() + + val resultNoSenderId = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = eventNoSenderId, + content = beaconLocationData, + roomId = "", + relatedEventId = "", + isLocalEcho = false + ) + val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = eventEmptySenderId, + content = beaconLocationData, + roomId = "", + relatedEventId = "", + isLocalEcho = false + ) + + resultNoSenderId shouldBeEqualTo false + resultEmptySenderId shouldBeEqualTo false + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt new file mode 100644 index 0000000000..febf94f4cf --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt @@ -0,0 +1,27 @@ +/* + * 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.test.fakes + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.internal.util.time.Clock + +internal class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt new file mode 100644 index 0000000000..9ba072d35c --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt @@ -0,0 +1,25 @@ +/* + * 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.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.di.WorkManagerProvider + +internal class FakeWorkManagerProvider { + + val instance = mockk() +} From dccc3b457debc693bf40650e9cc409c2807ae965 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 8 Jun 2022 11:45:35 +0200 Subject: [PATCH 159/314] Adding more tests on ignored cases --- .../LiveLocationAggregationProcessorTest.kt | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index d8c7f7477b..6a52a03880 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.test.fakes.FakeClock @@ -28,6 +29,7 @@ import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider private const val A_SESSION_ID = "session_id" private const val A_SENDER_ID = "sender_id" private const val AN_EVENT_ID = "event_id" +private const val A_ROOM_ID = "room_id" internal class LiveLocationAggregationProcessorTest { @@ -50,7 +52,7 @@ internal class LiveLocationAggregationProcessorTest { realm = fakeRealm.instance, event = event, content = beaconInfo, - roomId = "", + roomId = A_ROOM_ID, isLocalEcho = true ) @@ -67,14 +69,14 @@ internal class LiveLocationAggregationProcessorTest { realm = fakeRealm.instance, event = eventNoSenderId, content = beaconInfo, - roomId = "", + roomId = A_ROOM_ID, isLocalEcho = false ) val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconInfo( realm = fakeRealm.instance, event = eventEmptySenderId, content = beaconInfo, - roomId = "", + roomId = A_ROOM_ID, isLocalEcho = false ) @@ -82,6 +84,55 @@ internal class LiveLocationAggregationProcessorTest { resultEmptySenderId shouldBeEqualTo false } + @Test + fun `given beacon info when no target eventId is found then it is ignored`() { + val unsignedDataWithNoEventId = UnsignedData( + age = 123 + ) + val unsignedDataWithEmptyEventId = UnsignedData( + age = 123, + replacesState = "" + ) + val eventWithNoEventId = Event(senderId = A_SENDER_ID, unsignedData = unsignedDataWithNoEventId) + val eventWithEmptyEventId = Event(senderId = A_SENDER_ID, eventId = "", unsignedData = unsignedDataWithEmptyEventId) + val beaconInfoLive = MessageBeaconInfoContent(isLive = true) + val beaconInfoNotLive = MessageBeaconInfoContent(isLive = false) + + val resultLiveNoEventId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventWithNoEventId, + content = beaconInfoLive, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + val resultLiveEmptyEventId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventWithEmptyEventId, + content = beaconInfoLive, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + val resultNotLiveNoEventId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventWithNoEventId, + content = beaconInfoNotLive, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + val resultNotLiveEmptyEventId = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = eventWithEmptyEventId, + content = beaconInfoNotLive, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + + resultLiveNoEventId shouldBeEqualTo false + resultLiveEmptyEventId shouldBeEqualTo false + resultNotLiveNoEventId shouldBeEqualTo false + resultNotLiveEmptyEventId shouldBeEqualTo false + } + @Test fun `given beacon location data when it is local echo then it is ignored`() { val event = Event(senderId = A_SENDER_ID) @@ -91,14 +142,40 @@ internal class LiveLocationAggregationProcessorTest { realm = fakeRealm.instance, event = event, content = beaconLocationData, - roomId = "", - relatedEventId = "", + roomId = A_ROOM_ID, + relatedEventId = AN_EVENT_ID, isLocalEcho = true ) result shouldBeEqualTo false } + @Test + fun `given beacon location data when relatedEventId is null or empty then it is ignored`() { + val event = Event(senderId = A_SENDER_ID) + val beaconLocationData = MessageBeaconLocationDataContent() + + val resultNoRelatedEventId = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = null, + isLocalEcho = false + ) + val resultEmptyRelatedEventId = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = "", + isLocalEcho = false + ) + + resultNoRelatedEventId shouldBeEqualTo false + resultEmptyRelatedEventId shouldBeEqualTo false + } + @Test fun `given beacon location data and event when senderId is null or empty then it is ignored`() { val eventNoSenderId = Event(eventId = AN_EVENT_ID) @@ -110,7 +187,7 @@ internal class LiveLocationAggregationProcessorTest { event = eventNoSenderId, content = beaconLocationData, roomId = "", - relatedEventId = "", + relatedEventId = AN_EVENT_ID, isLocalEcho = false ) val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconLocationData( @@ -118,7 +195,7 @@ internal class LiveLocationAggregationProcessorTest { event = eventEmptySenderId, content = beaconLocationData, roomId = "", - relatedEventId = "", + relatedEventId = AN_EVENT_ID, isLocalEcho = false ) From 6386c1603f402dfc2dc4010b603e6efd83a5104e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 8 Jun 2022 16:56:29 +0200 Subject: [PATCH 160/314] Adding tests on beacon info aggregation --- ...cationShareAggregatedSummaryEntityQuery.kt | 1 + .../LiveLocationAggregationProcessorTest.kt | 120 ++++++++++++++++++ .../poll/PollAggregationProcessorTest.kt | 15 +-- .../android/sdk/test/fakes/FakeRealm.kt | 53 +++++++- .../sdk/test/fakes/FakeWorkManagerProvider.kt | 10 ++ 5 files changed, 185 insertions(+), 14 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 7dfeb6884a..6bcd737474 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -84,6 +84,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId) .findAll() + .toList() } /** diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 6a52a03880..1638e4b4b4 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -16,26 +16,37 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation +import androidx.work.OneTimeWorkRequest +import io.mockk.verify import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.test.fakes.FakeClock import org.matrix.android.sdk.test.fakes.FakeRealm import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenFindFirst +import org.matrix.android.sdk.test.fakes.givenNotEqualTo private const val A_SESSION_ID = "session_id" private const val A_SENDER_ID = "sender_id" private const val AN_EVENT_ID = "event_id" private const val A_ROOM_ID = "room_id" +private const val A_TIMESTAMP = 1654689143L +private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L internal class LiveLocationAggregationProcessorTest { private val fakeWorkManagerProvider = FakeWorkManagerProvider() private val fakeClock = FakeClock() private val fakeRealm = FakeRealm() + private val fakeQuery = fakeRealm.givenWhere() private val liveLocationAggregationProcessor = LiveLocationAggregationProcessor( sessionId = A_SESSION_ID, @@ -133,6 +144,85 @@ internal class LiveLocationAggregationProcessorTest { resultNotLiveEmptyEventId shouldBeEqualTo false } + @Test + fun `given beacon info and existing entity when beacon content is correct and active then it is aggregated`() { + val event = Event( + senderId = A_SENDER_ID, + eventId = AN_EVENT_ID + ) + val beaconInfo = MessageBeaconInfoContent( + isLive = true, + unstableTimestampMillis = A_TIMESTAMP, + timeout = A_TIMEOUT_MILLIS + ) + fakeClock.givenEpoch(A_TIMESTAMP + 5000) + val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() + val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() + + val result = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = event, + content = beaconInfo, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + + result shouldBeEqualTo true + aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID + aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID + aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID + aggregatedEntity.isActive shouldBeEqualTo true + aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS + aggregatedEntity.lastLocationContent shouldBeEqualTo null + previousEntities.forEach { entity -> + entity.isActive shouldBeEqualTo false + } + val workManager = fakeWorkManagerProvider.instance.workManager + verify { workManager.enqueueUniqueWork(any(), any(), any()) } + } + + @Test + fun `given beacon info and existing entity when beacon content is correct and inactive then it is aggregated`() { + val unsignedData = UnsignedData( + age = 123, + replacesState = AN_EVENT_ID + ) + val event = Event( + senderId = A_SENDER_ID, + eventId = "", + unsignedData = unsignedData + ) + val beaconInfo = MessageBeaconInfoContent( + isLive = false, + unstableTimestampMillis = A_TIMESTAMP, + timeout = A_TIMEOUT_MILLIS + ) + fakeClock.givenEpoch(A_TIMESTAMP + 5000) + val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() + val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() + + val result = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = event, + content = beaconInfo, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + + result shouldBeEqualTo true + aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID + aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID + aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID + aggregatedEntity.isActive shouldBeEqualTo false + aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS + aggregatedEntity.lastLocationContent shouldBeEqualTo null + previousEntities.forEach { entity -> + entity.isActive shouldBeEqualTo false + } + val workManager = fakeWorkManagerProvider.instance.workManager + verify { workManager.cancelUniqueWork(any()) } + } + @Test fun `given beacon location data when it is local echo then it is ignored`() { val event = Event(senderId = A_SENDER_ID) @@ -202,4 +292,34 @@ internal class LiveLocationAggregationProcessorTest { resultNoSenderId shouldBeEqualTo false resultEmptySenderId shouldBeEqualTo false } + + private fun mockLiveLocationShareAggregatedSummaryEntityForEvent(): LiveLocationShareAggregatedSummaryEntity { + val result = LiveLocationShareAggregatedSummaryEntity( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID + ) + fakeQuery + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(result) + return result + } + + private fun mockPreviousLiveLocationShareAggregatedSummaryEntities(): List { + val results = listOf( + LiveLocationShareAggregatedSummaryEntity( + eventId = "", + roomId = A_ROOM_ID, + userId = A_SENDER_ID, + isActive = true + ) + ) + fakeQuery + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) + .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .givenFindAll(results) + return results + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt index 837bbeea26..3044ca5d43 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt @@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.mockk.every import io.mockk.mockk import io.realm.RealmList -import io.realm.RealmModel -import io.realm.RealmQuery import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.Before @@ -46,6 +44,8 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst class PollAggregationProcessorTest { @@ -135,14 +135,11 @@ class PollAggregationProcessorTest { pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() } - private inline fun RealmQuery.givenEqualTo(fieldName: String, value: String, result: RealmQuery) { - every { equalTo(fieldName, value) } returns result - } - private fun mockEventAnnotationsSummaryEntity() { - val queryResult = realm.givenWhereReturns(result = EventAnnotationsSummaryEntity()) - queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!, queryResult) - queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!, queryResult) + realm.givenWhere() + .givenFindFirst(EventAnnotationsSummaryEntity()) + .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!) + .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!) } private fun mockRoom( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index c07f8e1873..1697921a8d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -21,16 +21,59 @@ import io.mockk.mockk import io.realm.Realm import io.realm.RealmModel import io.realm.RealmQuery +import io.realm.RealmResults import io.realm.kotlin.where internal class FakeRealm { val instance = mockk(relaxed = true) - inline fun givenWhereReturns(result: T?): RealmQuery { - val queryResult = mockk>() - every { queryResult.findFirst() } returns result - every { instance.where() } returns queryResult - return queryResult + inline fun givenWhere(): RealmQuery { + val query = mockk>() + every { instance.where() } returns query + return query } } + +inline fun RealmQuery.givenFindFirst( + result: T? +): RealmQuery { + every { findFirst() } returns result + return this +} + +inline fun RealmQuery.givenFindAll( + result: List +): RealmQuery { + val realmResults = mockk>() + result.forEachIndexed { index, t -> + every { realmResults[index] } returns t + } + every { realmResults.size } returns result.size + every { findAll() } returns realmResults + return this +} + +inline fun RealmQuery.givenEqualTo( + fieldName: String, + value: String +): RealmQuery { + every { equalTo(fieldName, value) } returns this + return this +} + +inline fun RealmQuery.givenEqualTo( + fieldName: String, + value: Boolean +): RealmQuery { + every { equalTo(fieldName, value) } returns this + return this +} + +inline fun RealmQuery.givenNotEqualTo( + fieldName: String, + value: String +): RealmQuery { + every { notEqualTo(fieldName, value) } returns this + return this +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt index 9ba072d35c..b6b435f531 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt @@ -16,10 +16,20 @@ package org.matrix.android.sdk.test.fakes +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.internal.di.WorkManagerProvider internal class FakeWorkManagerProvider { val instance = mockk() + + init { + val workManager = mockk() + every { workManager.enqueueUniqueWork(any(), any(), any()) } returns mockk() + every { workManager.cancelUniqueWork(any()) } returns mockk() + every { instance.workManager } returns workManager + } } From b9b1e2b3973e2a1d77218d5151cbadbc84714965 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 8 Jun 2022 17:43:10 +0200 Subject: [PATCH 161/314] Adding tests on location data aggregation --- .../LiveLocationAggregationProcessor.kt | 7 +- .../LiveLocationAggregationProcessorTest.kt | 68 ++++++++++++++++++- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index a254552bb3..921749122b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -151,12 +151,13 @@ internal class LiveLocationAggregationProcessor @Inject constructor( ?.getBestTimestampMillis() ?: 0 - if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) { + return if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) { Timber.d("updating last location of the summary of id=$relatedEventId") aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent()) + true + } else { + false } - - return true } private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 1638e4b4b4..b6e5ea8479 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -22,8 +22,12 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.LocationInfo import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.test.fakes.FakeClock @@ -40,6 +44,10 @@ private const val AN_EVENT_ID = "event_id" private const val A_ROOM_ID = "room_id" private const val A_TIMESTAMP = 1654689143L private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L +private const val A_LATITUDE = 40.05 +private const val A_LONGITUDE = 29.24 +private const val A_UNCERTAINTY = 30.0 +private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY" internal class LiveLocationAggregationProcessorTest { @@ -284,7 +292,7 @@ internal class LiveLocationAggregationProcessorTest { realm = fakeRealm.instance, event = eventEmptySenderId, content = beaconLocationData, - roomId = "", + roomId = A_ROOM_ID, relatedEventId = AN_EVENT_ID, isLocalEcho = false ) @@ -293,10 +301,64 @@ internal class LiveLocationAggregationProcessorTest { resultEmptySenderId shouldBeEqualTo false } - private fun mockLiveLocationShareAggregatedSummaryEntityForEvent(): LiveLocationShareAggregatedSummaryEntity { + @Test + fun `given beacon location data when location is less recent than the saved one then it is ignored`() { + val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID) + val beaconLocationData = MessageBeaconLocationDataContent( + unstableTimestampMillis = A_TIMESTAMP - 60_000 + ) + val lastBeaconLocationContent = MessageBeaconLocationDataContent( + unstableTimestampMillis = A_TIMESTAMP + ) + mockLiveLocationShareAggregatedSummaryEntityForEvent(lastBeaconLocationContent = lastBeaconLocationContent) + + val result = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = AN_EVENT_ID, + isLocalEcho = false + ) + + result shouldBeEqualTo false + } + + @Test + fun `given beacon location data when location is more recent than the saved one then it is aggregated`() { + val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID) + val locationInfo = LocationInfo(geoUri = A_GEO_URI) + val beaconLocationData = MessageBeaconLocationDataContent( + unstableTimestampMillis = A_TIMESTAMP, + unstableLocationInfo = locationInfo + ) + val lastBeaconLocationContent = MessageBeaconLocationDataContent( + unstableTimestampMillis = A_TIMESTAMP - 60_000 + ) + val entity = mockLiveLocationShareAggregatedSummaryEntityForEvent(lastBeaconLocationContent = lastBeaconLocationContent) + + val result = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = AN_EVENT_ID, + isLocalEcho = false + ) + + result shouldBeEqualTo true + val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel() + savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP + savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI + } + + private fun mockLiveLocationShareAggregatedSummaryEntityForEvent( + lastBeaconLocationContent: MessageBeaconLocationDataContent? = null + ): LiveLocationShareAggregatedSummaryEntity { val result = LiveLocationShareAggregatedSummaryEntity( eventId = AN_EVENT_ID, - roomId = A_ROOM_ID + roomId = A_ROOM_ID, + lastLocationContent = ContentMapper.map(lastBeaconLocationContent?.toContent()) ) fakeQuery .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) From e3981f42e99ecfde92f114ab927308abe8366e5e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 10 Jun 2022 15:17:26 +0200 Subject: [PATCH 162/314] Introducing FakeWorkManager --- .../LiveLocationAggregationProcessorTest.kt | 16 ++++--- .../android/sdk/test/fakes/FakeWorkManager.kt | 45 +++++++++++++++++++ .../sdk/test/fakes/FakeWorkManagerProvider.kt | 15 +++---- 3 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index b6e5ea8479..646a2194e1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -16,8 +16,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation -import androidx.work.OneTimeWorkRequest -import io.mockk.verify +import androidx.work.ExistingWorkPolicy import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event @@ -164,6 +163,7 @@ internal class LiveLocationAggregationProcessorTest { timeout = A_TIMEOUT_MILLIS ) fakeClock.givenEpoch(A_TIMESTAMP + 5000) + fakeWorkManagerProvider.fakeWorkManager.expectEnqueueUniqueWork() val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() @@ -185,8 +185,10 @@ internal class LiveLocationAggregationProcessorTest { previousEntities.forEach { entity -> entity.isActive shouldBeEqualTo false } - val workManager = fakeWorkManagerProvider.instance.workManager - verify { workManager.enqueueUniqueWork(any(), any(), any()) } + fakeWorkManagerProvider.fakeWorkManager.verifyEnqueueUniqueWork( + workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID), + policy = ExistingWorkPolicy.REPLACE + ) } @Test @@ -206,6 +208,7 @@ internal class LiveLocationAggregationProcessorTest { timeout = A_TIMEOUT_MILLIS ) fakeClock.givenEpoch(A_TIMESTAMP + 5000) + fakeWorkManagerProvider.fakeWorkManager.expectCancelUniqueWork() val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() @@ -227,8 +230,9 @@ internal class LiveLocationAggregationProcessorTest { previousEntities.forEach { entity -> entity.isActive shouldBeEqualTo false } - val workManager = fakeWorkManagerProvider.instance.workManager - verify { workManager.cancelUniqueWork(any()) } + fakeWorkManagerProvider.fakeWorkManager.verifyCancelUniqueWork( + workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) + ) } @Test diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt new file mode 100644 index 0000000000..b29d015a43 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt @@ -0,0 +1,45 @@ +/* + * 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.test.fakes + +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class FakeWorkManager { + + val instance = mockk() + + fun expectEnqueueUniqueWork() { + every { instance.enqueueUniqueWork(any(), any(), any()) } returns mockk() + } + + fun verifyEnqueueUniqueWork(workName: String, policy: ExistingWorkPolicy) { + verify { instance.enqueueUniqueWork(workName, policy, any()) } + } + + fun expectCancelUniqueWork() { + every { instance.cancelUniqueWork(any()) } returns mockk() + } + + fun verifyCancelUniqueWork(workName: String) { + verify { instance.cancelUniqueWork(workName) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt index b6b435f531..51ff24c01d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt @@ -16,20 +16,15 @@ package org.matrix.android.sdk.test.fakes -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.internal.di.WorkManagerProvider -internal class FakeWorkManagerProvider { +internal class FakeWorkManagerProvider( + val fakeWorkManager: FakeWorkManager = FakeWorkManager(), +) { - val instance = mockk() - - init { - val workManager = mockk() - every { workManager.enqueueUniqueWork(any(), any(), any()) } returns mockk() - every { workManager.cancelUniqueWork(any()) } returns mockk() - every { instance.workManager } returns workManager + val instance = mockk().also { + every { it.workManager } returns fakeWorkManager.instance } } From 2c961793832e37884e418f3ef3f2c457f3084d2e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 10 Jun 2022 16:41:44 +0200 Subject: [PATCH 163/314] Renaming helpers to clarify purpose --- .../LiveLocationAggregationProcessorTest.kt | 71 ++++++++++++------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 646a2194e1..dd4ece6af3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -164,8 +164,17 @@ internal class LiveLocationAggregationProcessorTest { ) fakeClock.givenEpoch(A_TIMESTAMP + 5000) fakeWorkManagerProvider.fakeWorkManager.expectEnqueueUniqueWork() - val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() - val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() + val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) + val previousEntities = givenActiveSummaryListQueryReturns( + listOf( + LiveLocationShareAggregatedSummaryEntity( + eventId = "${AN_EVENT_ID}1", + roomId = A_ROOM_ID, + userId = A_SENDER_ID, + isActive = true + ) + ) + ) val result = liveLocationAggregationProcessor.handleBeaconInfo( realm = fakeRealm.instance, @@ -209,8 +218,18 @@ internal class LiveLocationAggregationProcessorTest { ) fakeClock.givenEpoch(A_TIMESTAMP + 5000) fakeWorkManagerProvider.fakeWorkManager.expectCancelUniqueWork() - val aggregatedEntity = mockLiveLocationShareAggregatedSummaryEntityForEvent() - val previousEntities = mockPreviousLiveLocationShareAggregatedSummaryEntities() + val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) + val previousEntities = givenActiveSummaryListQueryReturns( + listOf( + LiveLocationShareAggregatedSummaryEntity( + eventId = "${AN_EVENT_ID}1", + roomId = A_ROOM_ID, + userId = A_SENDER_ID, + isActive = true + ) + ) + + ) val result = liveLocationAggregationProcessor.handleBeaconInfo( realm = fakeRealm.instance, @@ -314,7 +333,11 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP ) - mockLiveLocationShareAggregatedSummaryEntityForEvent(lastBeaconLocationContent = lastBeaconLocationContent) + givenLastSummaryQueryReturns( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + beaconLocationContent = lastBeaconLocationContent + ) val result = liveLocationAggregationProcessor.handleBeaconLocationData( realm = fakeRealm.instance, @@ -339,7 +362,11 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP - 60_000 ) - val entity = mockLiveLocationShareAggregatedSummaryEntityForEvent(lastBeaconLocationContent = lastBeaconLocationContent) + val entity = givenLastSummaryQueryReturns( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + beaconLocationContent = lastBeaconLocationContent + ) val result = liveLocationAggregationProcessor.handleBeaconLocationData( realm = fakeRealm.instance, @@ -356,36 +383,32 @@ internal class LiveLocationAggregationProcessorTest { savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI } - private fun mockLiveLocationShareAggregatedSummaryEntityForEvent( - lastBeaconLocationContent: MessageBeaconLocationDataContent? = null + private fun givenLastSummaryQueryReturns( + eventId: String, + roomId: String, + beaconLocationContent: MessageBeaconLocationDataContent? = null ): LiveLocationShareAggregatedSummaryEntity { val result = LiveLocationShareAggregatedSummaryEntity( - eventId = AN_EVENT_ID, - roomId = A_ROOM_ID, - lastLocationContent = ContentMapper.map(lastBeaconLocationContent?.toContent()) + eventId = eventId, + roomId = roomId, + lastLocationContent = ContentMapper.map(beaconLocationContent?.toContent()) ) fakeQuery - .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) - .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId) .givenFindFirst(result) return result } - private fun mockPreviousLiveLocationShareAggregatedSummaryEntities(): List { - val results = listOf( - LiveLocationShareAggregatedSummaryEntity( - eventId = "", - roomId = A_ROOM_ID, - userId = A_SENDER_ID, - isActive = true - ) - ) + private fun givenActiveSummaryListQueryReturns( + summaryList: List + ): List { fakeQuery .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) - .givenFindAll(results) - return results + .givenFindAll(summaryList) + return summaryList } } From ac4b33647d9828e82a7695e7626b6ed3f75a1ab3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 10:09:09 +0200 Subject: [PATCH 164/314] Mutualizing some similar tests with different parameters --- .../LiveLocationAggregationProcessorTest.kt | 219 +++++++++--------- 1 file changed, 105 insertions(+), 114 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index dd4ece6af3..e6d63f5e5e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -77,78 +77,71 @@ internal class LiveLocationAggregationProcessorTest { result shouldBeEqualTo false } - @Test - fun `given beacon info and event when senderId is null or empty then it is ignored`() { - val eventNoSenderId = Event(eventId = AN_EVENT_ID) - val eventEmptySenderId = Event(eventId = AN_EVENT_ID, senderId = "") - val beaconInfo = MessageBeaconInfoContent() - - val resultNoSenderId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventNoSenderId, - content = beaconInfo, - roomId = A_ROOM_ID, - isLocalEcho = false - ) - val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventEmptySenderId, - content = beaconInfo, - roomId = A_ROOM_ID, - isLocalEcho = false - ) - - resultNoSenderId shouldBeEqualTo false - resultEmptySenderId shouldBeEqualTo false - } + private data class IgnoredBeaconInfoEvent( + val event: Event, + val beaconInfo: MessageBeaconInfoContent + ) @Test - fun `given beacon info when no target eventId is found then it is ignored`() { - val unsignedDataWithNoEventId = UnsignedData( - age = 123 - ) - val unsignedDataWithEmptyEventId = UnsignedData( - age = 123, - replacesState = "" - ) - val eventWithNoEventId = Event(senderId = A_SENDER_ID, unsignedData = unsignedDataWithNoEventId) - val eventWithEmptyEventId = Event(senderId = A_SENDER_ID, eventId = "", unsignedData = unsignedDataWithEmptyEventId) - val beaconInfoLive = MessageBeaconInfoContent(isLive = true) - val beaconInfoNotLive = MessageBeaconInfoContent(isLive = false) - - val resultLiveNoEventId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventWithNoEventId, - content = beaconInfoLive, - roomId = A_ROOM_ID, - isLocalEcho = false - ) - val resultLiveEmptyEventId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventWithEmptyEventId, - content = beaconInfoLive, - roomId = A_ROOM_ID, - isLocalEcho = false - ) - val resultNotLiveNoEventId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventWithNoEventId, - content = beaconInfoNotLive, - roomId = A_ROOM_ID, - isLocalEcho = false - ) - val resultNotLiveEmptyEventId = liveLocationAggregationProcessor.handleBeaconInfo( - realm = fakeRealm.instance, - event = eventWithEmptyEventId, - content = beaconInfoNotLive, - roomId = A_ROOM_ID, - isLocalEcho = false + fun `given beacon info and event when some values are missing then it is ignored`() { + val ignoredInfoEvents = listOf( + // missing senderId + IgnoredBeaconInfoEvent( + event = Event(eventId = AN_EVENT_ID, senderId = null), + beaconInfo = MessageBeaconInfoContent() + ), + // empty senderId + IgnoredBeaconInfoEvent( + event = Event(eventId = AN_EVENT_ID, senderId = ""), + beaconInfo = MessageBeaconInfoContent() + ), + // beacon is live and no eventId + IgnoredBeaconInfoEvent( + event = Event(eventId = null, senderId = A_SENDER_ID), + beaconInfo = MessageBeaconInfoContent(isLive = true) + ), + // beacon is live and eventId is empty + IgnoredBeaconInfoEvent( + event = Event(eventId = "", senderId = A_SENDER_ID), + beaconInfo = MessageBeaconInfoContent(isLive = true) + ), + // beacon is not live and replaced event id is null + IgnoredBeaconInfoEvent( + event = Event( + eventId = AN_EVENT_ID, + senderId = A_SENDER_ID, + unsignedData = UnsignedData( + age = 123, + replacesState = null + ) + ), + beaconInfo = MessageBeaconInfoContent(isLive = false) + ), + // beacon is not live and replaced event id is empty + IgnoredBeaconInfoEvent( + event = Event( + eventId = AN_EVENT_ID, + senderId = A_SENDER_ID, + unsignedData = UnsignedData( + age = 123, + replacesState = "" + ) + ), + beaconInfo = MessageBeaconInfoContent(isLive = false) + ), ) - resultLiveNoEventId shouldBeEqualTo false - resultLiveEmptyEventId shouldBeEqualTo false - resultNotLiveNoEventId shouldBeEqualTo false - resultNotLiveEmptyEventId shouldBeEqualTo false + ignoredInfoEvents.forEach { + val result = liveLocationAggregationProcessor.handleBeaconInfo( + realm = fakeRealm.instance, + event = it.event, + content = it.beaconInfo, + roomId = A_ROOM_ID, + isLocalEcho = false + ) + + result shouldBeEqualTo false + } } @Test @@ -271,57 +264,55 @@ internal class LiveLocationAggregationProcessorTest { result shouldBeEqualTo false } + private data class IgnoredBeaconLocationDataEvent( + val event: Event, + val beaconLocationData: MessageBeaconLocationDataContent + ) + + @Test + fun `given event and beacon location data when some values are missing then it is ignored`() { + val ignoredLocationDataEvents = listOf( + // missing sender id + IgnoredBeaconLocationDataEvent( + event = Event(eventId = AN_EVENT_ID), + beaconLocationData = MessageBeaconLocationDataContent() + ), + // empty sender id + IgnoredBeaconLocationDataEvent( + event = Event(eventId = AN_EVENT_ID, senderId = ""), + beaconLocationData = MessageBeaconLocationDataContent() + ), + ) + + ignoredLocationDataEvents.forEach { + val result = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = it.event, + content = it.beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = "", + isLocalEcho = false + ) + result shouldBeEqualTo false + } + } + @Test fun `given beacon location data when relatedEventId is null or empty then it is ignored`() { val event = Event(senderId = A_SENDER_ID) val beaconLocationData = MessageBeaconLocationDataContent() - val resultNoRelatedEventId = liveLocationAggregationProcessor.handleBeaconLocationData( - realm = fakeRealm.instance, - event = event, - content = beaconLocationData, - roomId = A_ROOM_ID, - relatedEventId = null, - isLocalEcho = false - ) - val resultEmptyRelatedEventId = liveLocationAggregationProcessor.handleBeaconLocationData( - realm = fakeRealm.instance, - event = event, - content = beaconLocationData, - roomId = A_ROOM_ID, - relatedEventId = "", - isLocalEcho = false - ) - - resultNoRelatedEventId shouldBeEqualTo false - resultEmptyRelatedEventId shouldBeEqualTo false - } - - @Test - fun `given beacon location data and event when senderId is null or empty then it is ignored`() { - val eventNoSenderId = Event(eventId = AN_EVENT_ID) - val eventEmptySenderId = Event(eventId = AN_EVENT_ID, senderId = "") - val beaconLocationData = MessageBeaconLocationDataContent() - - val resultNoSenderId = liveLocationAggregationProcessor.handleBeaconLocationData( - realm = fakeRealm.instance, - event = eventNoSenderId, - content = beaconLocationData, - roomId = "", - relatedEventId = AN_EVENT_ID, - isLocalEcho = false - ) - val resultEmptySenderId = liveLocationAggregationProcessor.handleBeaconLocationData( - realm = fakeRealm.instance, - event = eventEmptySenderId, - content = beaconLocationData, - roomId = A_ROOM_ID, - relatedEventId = AN_EVENT_ID, - isLocalEcho = false - ) - - resultNoSenderId shouldBeEqualTo false - resultEmptySenderId shouldBeEqualTo false + listOf(null, "").forEach { + val result = liveLocationAggregationProcessor.handleBeaconLocationData( + realm = fakeRealm.instance, + event = event, + content = beaconLocationData, + roomId = A_ROOM_ID, + relatedEventId = it, + isLocalEcho = false + ) + result shouldBeEqualTo false + } } @Test From 3db8e0b045032121d7adedd49462476be84c32d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jun 2022 23:18:10 +0000 Subject: [PATCH 165/314] Bump flipper-network-plugin from 0.149.0 to 0.150.0 Bumps [flipper-network-plugin](https://github.com/facebook/flipper) from 0.149.0 to 0.150.0. - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.149.0...v0.150.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 8d704141e5..f3761c4c78 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -532,7 +532,7 @@ dependencies { debugImplementation('com.facebook.flipper:flipper:0.149.0') { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - debugImplementation('com.facebook.flipper:flipper-network-plugin:0.149.0') { + debugImplementation('com.facebook.flipper:flipper-network-plugin:0.150.0') { exclude group: 'com.facebook.fbjni', module: 'fbjni' } debugImplementation 'com.facebook.soloader:soloader:0.10.3' From 29f48249e2670c99a0ea91af53b9c9cdc6d21d9f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 15:32:28 +0200 Subject: [PATCH 166/314] Start live location share API --- .../room/location/LocationSharingService.kt | 10 ++++ .../api/session/room/state/StateService.kt | 2 + .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../location/DefaultLocationSharingService.kt | 9 +++ .../location/StartLiveLocationShareTask.kt | 57 +++++++++++++++++++ .../location/LocationSharingService.kt | 20 +------ 6 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index dd48d51f45..418ba6fbf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -23,5 +23,15 @@ import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationSh * Manage all location sharing related features. */ interface LocationSharingService { + /** + * Starts sharing live location in the room. + * @param timeoutMillis timeout of the live in milliseconds + * @return the id of the created beacon info event + */ + suspend fun startLiveLocationShare(timeoutMillis: Long): String + + /** + * Returns a LiveData on the list of current running live location shares. + */ fun getRunningLiveLocationShareSummaries(): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 49c0debe1b..8fb3afe36b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -66,12 +66,14 @@ interface StateService { */ suspend fun deleteAvatar() + // TODO delete /** * Stops sharing live location in the room. * @param userId user id */ suspend fun stopLiveLocation(userId: String) + // TODO delete /** * Returns beacon info state event of a user. * @param userId user id who is sharing location diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index f3845f1f15..164b6cd4e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -51,6 +51,8 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask @@ -299,4 +301,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask + + @Binds + abstract fun bindStartLiveLocationShareTask(task: DefaultStartLiveLocationShareTask): StartLiveLocationShareTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 8cf6fcdfbf..078c261b8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase internal class DefaultLocationSharingService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, + private val startLiveLocationShareTask: StartLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -40,6 +41,14 @@ internal class DefaultLocationSharingService @AssistedInject constructor( fun create(roomId: String): DefaultLocationSharingService } + override suspend fun startLiveLocationShare(timeoutMillis: Long): String { + val params = StartLiveLocationShareTask.Params( + roomId = roomId, + timeoutMillis = timeoutMillis + ) + return startLiveLocationShareTask.execute(params) + } + override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt new file mode 100644 index 0000000000..85bd90cf20 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -0,0 +1,57 @@ +/* + * 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.internal.session.room.location + +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +internal interface StartLiveLocationShareTask : Task { + data class Params( + val roomId: String, + val timeoutMillis: Long, + ) +} + +// TODO add unit tests +internal class DefaultStartLiveLocationShareTask @Inject constructor( + @UserId private val userId: String, + private val clock: Clock, + private val sendStateTask: SendStateTask +) : StartLiveLocationShareTask { + + override suspend fun execute(params: StartLiveLocationShareTask.Params): String { + val beaconContent = MessageBeaconInfoContent( + timeout = params.timeoutMillis, + isLive = true, + unstableTimestampMillis = clock.epochMillis() + ).toContent() + val eventType = EventType.STATE_ROOM_BEACON_INFO.first() + val sendStateTaskParams = SendStateTask.Params( + roomId = params.roomId, + stateKey = userId, + eventType = eventType, + body = beaconContent + ) + return sendStateTask.executeRetry(sendStateTaskParams, 3) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 62aba9318c..c9479f4536 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -23,16 +23,12 @@ import android.os.Parcelable import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorService -import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import timber.log.Timber import java.util.Timer import java.util.TimerTask @@ -51,7 +47,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var locationTracker: LocationTracker @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var clock: Clock private val binder = LocalBinder() @@ -97,21 +92,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { - val beaconContent = MessageBeaconInfoContent( - timeout = roomArgs.durationMillis, - isLive = true, - unstableTimestampMillis = clock.epochMillis() - ).toContent() - - val stateKey = session.myUserId val beaconEventId = session .getRoom(roomArgs.roomId) - ?.stateService() - ?.sendStateEvent( - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), - stateKey = stateKey, - body = beaconContent - ) + ?.locationSharingService() + ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis) beaconEventId ?.takeUnless { it.isEmpty() } From 632064ffdedfa03b0d920cc96968c57e4d9edfed Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 16:19:40 +0200 Subject: [PATCH 167/314] Stop live location share API --- .../room/location/LocationSharingService.kt | 5 ++ .../api/session/room/state/StateService.kt | 15 ---- .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../location/DefaultLocationSharingService.kt | 8 +++ .../location/StartLiveLocationShareTask.kt | 2 +- .../location/StopLiveLocationShareTask.kt | 72 +++++++++++++++++++ .../session/room/state/DefaultStateService.kt | 37 ---------- .../location/LocationSharingService.kt | 4 +- 8 files changed, 94 insertions(+), 54 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 418ba6fbf2..5f80aa5c9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -30,6 +30,11 @@ interface LocationSharingService { */ suspend fun startLiveLocationShare(timeoutMillis: Long): String + /** + * Stops sharing live location in the room. + */ + suspend fun stopLiveLocationShare() + /** * Returns a LiveData on the list of current running live location shares. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 8fb3afe36b..6ca63c2c49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -66,21 +66,6 @@ interface StateService { */ suspend fun deleteAvatar() - // TODO delete - /** - * Stops sharing live location in the room. - * @param userId user id - */ - suspend fun stopLiveLocation(userId: String) - - // TODO delete - /** - * Returns beacon info state event of a user. - * @param userId user id who is sharing location - * @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included - */ - suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? - /** * Send a state event to the room. * @param eventType The type of event to send. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 164b6cd4e5..0c85993c54 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -52,7 +52,9 @@ import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask @@ -304,4 +306,7 @@ internal abstract class RoomModule { @Binds abstract fun bindStartLiveLocationShareTask(task: DefaultStartLiveLocationShareTask): StartLiveLocationShareTask + + @Binds + abstract fun bindStopLiveLocationShareTask(task: DefaultStopLiveLocationShareTask): StopLiveLocationShareTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 078c261b8a..43a10f6785 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -33,6 +33,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, private val startLiveLocationShareTask: StartLiveLocationShareTask, + private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -49,6 +50,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return startLiveLocationShareTask.execute(params) } + override suspend fun stopLiveLocationShare() { + val params = StopLiveLocationShareTask.Params( + roomId = roomId, + ) + return stopLiveLocationShareTask.execute(params) + } + override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index 85bd90cf20..c13f625a41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -36,7 +36,7 @@ internal interface StartLiveLocationShareTask : Task { + data class Params( + val roomId: String, + ) +} + +// TODO add unit tests +internal class DefaultStopLiveLocationShareTask @Inject constructor( + @UserId private val userId: String, + private val sendStateTask: SendStateTask, + private val stateEventDataSource: StateEventDataSource, +) : StopLiveLocationShareTask { + + override suspend fun execute(params: StopLiveLocationShareTask.Params) { + val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return + val stateKey = beaconInfoStateEvent.stateKey ?: return + val content = beaconInfoStateEvent.getClearContent()?.toModel() ?: return + val updatedContent = content.copy(isLive = false).toContent() + val sendStateTaskParams = SendStateTask.Params( + roomId = params.roomId, + stateKey = stateKey, + eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + body = updatedContent + ) + sendStateTask.executeRetry(sendStateTaskParams, 3) + } + + private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? { + return EventType.STATE_ROOM_BEACON_INFO + .mapNotNull { + stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = it, + stateKey = QueryStringValue.Equals(userId) + ) + } + .firstOrNull { beaconInfoEvent -> + beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index c15bcb1c1a..ad47b82428 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -21,33 +21,27 @@ import androidx.lifecycle.LiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStateEventValue -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.content.FileUploader -import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder internal class DefaultStateService @AssistedInject constructor( @Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, private val sendStateTask: SendStateTask, private val fileUploader: FileUploader, - private val viaParameterFinder: ViaParameterFinder ) : StateService { @AssistedFactory @@ -191,35 +185,4 @@ internal class DefaultStateService @AssistedInject constructor( } updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries) } - - override suspend fun stopLiveLocation(userId: String) { - getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent -> - beaconInfoStateEvent.getClearContent()?.toModel()?.let { content -> - val updatedContent = content.copy(isLive = false).toContent() - - beaconInfoStateEvent.stateKey?.let { - sendStateEvent( - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), - body = updatedContent, - stateKey = it - ) - } - } - } - } - - override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? { - return EventType.STATE_ROOM_BEACON_INFO - .mapNotNull { - stateEventDataSource.getStateEvent( - roomId = roomId, - eventType = it, - stateKey = QueryStringValue.Equals(userId) - ) - } - .firstOrNull { beaconInfoEvent -> - !filterOnlyLive || - beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse() - } - } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index c9479f4536..39fc526f80 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -147,7 +147,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { .getSafeActiveSession() ?.let { session -> session.coroutineScope.launch(session.coroutineDispatchers.io) { - session.getRoom(roomId)?.stateService()?.stopLiveLocation(session.myUserId) + session.getRoom(roomId) + ?.locationSharingService() + ?.stopLiveLocationShare() } } } From 9b61c1aeadf7066314abb19ed4c1d59160f6272d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 17:10:33 +0200 Subject: [PATCH 168/314] Send static location API --- .../room/location/LocationSharingService.kt | 10 ++++ .../sdk/api/session/room/send/SendService.kt | 9 ---- .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../location/DefaultLocationSharingService.kt | 13 +++++ .../room/location/SendStaticLocationTask.kt | 52 +++++++++++++++++++ .../session/room/send/DefaultSendService.kt | 6 --- .../location/LocationSharingViewModel.kt | 16 +++--- 7 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 5f80aa5c9b..3ff629ac7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -18,11 +18,21 @@ package org.matrix.android.sdk.api.session.room.location import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.util.Cancelable /** * Manage all location sharing related features. */ interface LocationSharingService { + /** + * Send a static location event to the room. + * @param latitude required latitude of the location + * @param longitude required longitude of the location + * @param uncertainty Accuracy of the location in meters + * @param isUserLocation indicates whether the location data corresponds to the user location or not (pinned location) + */ + suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable + /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 661c3be5bd..201e6c5c0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -142,15 +142,6 @@ interface SendService { */ fun resendMediaMessage(localEcho: TimelineEvent): Cancelable - /** - * Send a location event to the room. - * @param latitude required latitude of the location - * @param longitude required longitude of the location - * @param uncertainty Accuracy of the location in meters - * @param isUserLocation indicates whether the location data corresponds to the user location or not - */ - fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable - /** * Send a live location event to the room. beacon_info state event has to be sent before sending live location updates. * @param beaconInfoEventId event id of the initial beacon info state event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 0c85993c54..ee332890e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -51,8 +51,10 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask @@ -309,4 +311,7 @@ internal abstract class RoomModule { @Binds abstract fun bindStopLiveLocationShareTask(task: DefaultStopLiveLocationShareTask): StopLiveLocationShareTask + + @Binds + abstract fun bindSendStaticLocationTask(task: DefaultSendStaticLocationTask): SendStaticLocationTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 43a10f6785..407ae60e4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -23,6 +23,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom @@ -32,6 +33,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase internal class DefaultLocationSharingService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, + private val sendStaticLocationTask: SendStaticLocationTask, private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, @@ -42,6 +44,17 @@ internal class DefaultLocationSharingService @AssistedInject constructor( fun create(roomId: String): DefaultLocationSharingService } + override suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable { + val params = SendStaticLocationTask.Params( + roomId = roomId, + latitude = latitude, + longitude = longitude, + uncertainty = uncertainty, + isUserLocation = isUserLocation + ) + return sendStaticLocationTask.execute(params) + } + override suspend fun startLiveLocationShare(timeoutMillis: Long): String { val params = StartLiveLocationShareTask.Params( roomId = roomId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt new file mode 100644 index 0000000000..d26609196f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt @@ -0,0 +1,52 @@ +/* + * 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.internal.session.room.location + +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface SendStaticLocationTask : Task { + data class Params( + val roomId: String, + val latitude: Double, + val longitude: Double, + val uncertainty: Double?, + val isUserLocation: Boolean + ) +} + +// TODO add unit tests +internal class DefaultSendStaticLocationTask @Inject constructor( + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, +) : SendStaticLocationTask { + + override suspend fun execute(params: SendStaticLocationTask.Params): Cancelable { + val event = localEchoEventFactory.createLocationEvent( + roomId = params.roomId, + latitude = params.latitude, + longitude = params.longitude, + uncertainty = params.uncertainty, + isUserLocation = params.isUserLocation + ) + localEchoEventFactory.createLocalEcho(event) + return eventSenderProcessor.postEvent(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index fc78abcfd9..e83175151e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -129,12 +129,6 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable { - return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation) - .also { createLocalEcho(it) } - .let { sendEvent(it) } - } - override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty) .also { createLocalEcho(it) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 71f59c6fdf..30476d064f 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -136,13 +136,15 @@ class LocationSharingViewModel @AssistedInject constructor( private fun shareLocation(locationData: LocationData?, isUserLocation: Boolean) { locationData?.let { location -> - room.sendService().sendLocation( - latitude = location.latitude, - longitude = location.longitude, - uncertainty = location.uncertainty, - isUserLocation = isUserLocation - ) - _viewEvents.post(LocationSharingViewEvents.Close) + viewModelScope.launch { + room.locationSharingService().sendStaticLocation( + latitude = location.latitude, + longitude = location.longitude, + uncertainty = location.uncertainty, + isUserLocation = isUserLocation + ) + _viewEvents.post(LocationSharingViewEvents.Close) + } } ?: run { _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) } From 7b159c5b71ec879b25ee7435d3ec80194072037a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 17:40:14 +0200 Subject: [PATCH 169/314] Send live location API --- .../room/location/LocationSharingService.kt | 10 ++++ .../sdk/api/session/room/send/SendService.kt | 9 --- .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../location/DefaultLocationSharingService.kt | 14 ++++- .../room/location/SendLiveLocationTask.kt | 52 +++++++++++++++++ .../session/room/send/DefaultSendService.kt | 6 -- .../location/LocationSharingService.kt | 57 +++++++++---------- 7 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 3ff629ac7a..11b74ecd7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -33,6 +33,16 @@ interface LocationSharingService { */ suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable + /** + * Send a live location event to the room. + * To get the beacon info event id, [startLiveLocationShare] must be called before sending live location updates. + * @param beaconInfoEventId event id of the initial beacon info state event + * @param latitude required latitude of the location + * @param longitude required longitude of the location + * @param uncertainty Accuracy of the location in meters + */ + suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable + /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 201e6c5c0c..9cf062356f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -142,15 +142,6 @@ interface SendService { */ fun resendMediaMessage(localEcho: TimelineEvent): Cancelable - /** - * Send a live location event to the room. beacon_info state event has to be sent before sending live location updates. - * @param beaconInfoEventId event id of the initial beacon info state event - * @param latitude required latitude of the location - * @param longitude required longitude of the location - * @param uncertainty Accuracy of the location in meters - */ - fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable - /** * Remove this failed message from the timeline. * @param localEcho the unsent local echo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index ee332890e4..271e82a1e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -51,9 +51,11 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask @@ -314,4 +316,7 @@ internal abstract class RoomModule { @Binds abstract fun bindSendStaticLocationTask(task: DefaultSendStaticLocationTask): SendStaticLocationTask + + @Binds + abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 407ae60e4b..ba6e7bd3fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -34,6 +34,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, private val sendStaticLocationTask: SendStaticLocationTask, + private val sendLiveLocationTask: SendLiveLocationTask, private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, @@ -50,11 +51,22 @@ internal class DefaultLocationSharingService @AssistedInject constructor( latitude = latitude, longitude = longitude, uncertainty = uncertainty, - isUserLocation = isUserLocation + isUserLocation = isUserLocation, ) return sendStaticLocationTask.execute(params) } + override suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { + val params = SendLiveLocationTask.Params( + beaconInfoEventId = beaconInfoEventId, + roomId = roomId, + latitude = latitude, + longitude = longitude, + uncertainty = uncertainty, + ) + return sendLiveLocationTask.execute(params) + } + override suspend fun startLiveLocationShare(timeoutMillis: Long): String { val params = StartLiveLocationShareTask.Params( roomId = roomId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt new file mode 100644 index 0000000000..ed40c82b66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt @@ -0,0 +1,52 @@ +/* + * 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.internal.session.room.location + +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface SendLiveLocationTask : Task { + data class Params( + val roomId: String, + val beaconInfoEventId: String, + val latitude: Double, + val longitude: Double, + val uncertainty: Double?, + ) +} + +// TODO add unit tests +internal class DefaultSendLiveLocationTask @Inject constructor( + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, +) : SendLiveLocationTask { + + override suspend fun execute(params: SendLiveLocationTask.Params): Cancelable { + val event = localEchoEventFactory.createLiveLocationEvent( + beaconInfoEventId = params.beaconInfoEventId, + roomId = params.roomId, + latitude = params.latitude, + longitude = params.longitude, + uncertainty = params.uncertainty, + ) + localEchoEventFactory.createLocalEcho(event) + return eventSenderProcessor.postEvent(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index e83175151e..418000abed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -129,12 +129,6 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { - return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty) - .also { createLocalEcho(it) } - .let { sendEvent(it) } - } - override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 39fc526f80..77f3abcc28 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -25,6 +25,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorService import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.Session @@ -79,13 +80,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { scheduleTimer(roomArgs.roomId, roomArgs.durationMillis) // Send beacon info state event - activeSessionHolder - .getSafeActiveSession() - ?.let { session -> - session.coroutineScope.launch(session.coroutineDispatchers.io) { - sendStartingLiveBeaconInfo(session, roomArgs) - } - } + launchInIO { session -> + sendStartingLiveBeaconInfo(session, roomArgs) + } } return START_STICKY @@ -143,15 +140,11 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } private fun sendStoppedBeaconInfo(roomId: String) { - activeSessionHolder - .getSafeActiveSession() - ?.let { session -> - session.coroutineScope.launch(session.coroutineDispatchers.io) { - session.getRoom(roomId) - ?.locationSharingService() - ?.stopLiveLocationShare() - } - } + launchInIO { session -> + session.getRoom(roomId) + ?.locationSharingService() + ?.stopLiveLocationShare() + } } override fun onLocationUpdate(locationData: LocationData) { @@ -168,20 +161,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { beaconInfoEventId: String, locationData: LocationData ) { - val session = activeSessionHolder.getSafeActiveSession() - val room = session?.getRoom(roomId) - val userId = session?.myUserId - - if (room == null || userId == null) { - return + launchInIO { session -> + session.getRoom(roomId) + ?.locationSharingService() + ?.sendLiveLocation( + beaconInfoEventId = beaconInfoEventId, + latitude = locationData.latitude, + longitude = locationData.longitude, + uncertainty = locationData.uncertainty + ) } - - room.sendService().sendLiveLocation( - beaconInfoEventId = beaconInfoEventId, - latitude = locationData.latitude, - longitude = locationData.longitude, - uncertainty = locationData.uncertainty - ) } override fun onNoLocationProviderAvailable() { @@ -202,6 +191,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { destroyMe() } + private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = + activeSessionHolder + .getSafeActiveSession() + ?.let { session -> + session.coroutineScope.launch( + context = session.coroutineDispatchers.io, + block = { block(session) } + ) + } + override fun onBind(intent: Intent?): IBinder { return binder } From 752434acb4d84cbc58d691bfd87d1e3c8eda072f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 9 Jun 2022 17:44:24 +0200 Subject: [PATCH 170/314] Adding changelog entry --- changelog.d/5864.sdk | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5864.sdk diff --git a/changelog.d/5864.sdk b/changelog.d/5864.sdk new file mode 100644 index 0000000000..b0a9d1c67d --- /dev/null +++ b/changelog.d/5864.sdk @@ -0,0 +1 @@ +Group all location sharing related API into LocationSharingService From 7d4df8be0910dea6b4a6d6e924fec87d0082dd5e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 16:32:45 +0200 Subject: [PATCH 171/314] Unit test for method to get live data of active lives --- ...iveLocationShareAggregatedSummaryMapper.kt | 6 +- .../location/DefaultLocationSharingService.kt | 3 +- .../DefaultLocationSharingServiceTest.kt | 85 +++++++++++++++++++ .../android/sdk/test/fakes/FakeMonarchy.kt | 40 +++++++-- .../android/sdk/test/fakes/FakeRealm.kt | 63 ++++++++++++++ 5 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt index 9460e4c6ba..4a4c730a0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt @@ -16,15 +16,17 @@ package org.matrix.android.sdk.internal.database.mapper +import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import javax.inject.Inject -internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() { +internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() : + Monarchy.Mapper { - fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { + override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { return LiveLocationShareAggregatedSummary( userId = entity.userId, isActive = entity.isActive, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index ba6e7bd3fa..3fa00fa077 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationS import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom import org.matrix.android.sdk.internal.di.SessionDatabase -// TODO add unit tests internal class DefaultLocationSharingService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, @@ -85,7 +84,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, - { liveLocationShareAggregatedSummaryMapper.map(it) } + liveLocationShareAggregatedSummaryMapper ) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt new file mode 100644 index 0000000000..42db5761d7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -0,0 +1,85 @@ +/* + * 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.internal.session.room.location + +import io.mockk.mockk +import io.mockk.unmockkAll +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenIsNotEmpty +import org.matrix.android.sdk.test.fakes.givenIsNotNull + +private const val A_ROOM_ID = "room_id" + +internal class DefaultLocationSharingServiceTest { + + private val fakeRoomId = A_ROOM_ID + private val fakeMonarchy = FakeMonarchy() + private val sendStaticLocationTask = mockk() + private val sendLiveLocationTask = mockk() + private val startLiveLocationShareTask = mockk() + private val stopLiveLocationShareTask = mockk() + private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() + + private val defaultLocationSharingService = DefaultLocationSharingService( + roomId = fakeRoomId, + monarchy = fakeMonarchy.instance, + sendStaticLocationTask = sendStaticLocationTask, + sendLiveLocationTask = sendLiveLocationTask, + startLiveLocationShareTask = startLiveLocationShareTask, + stopLiveLocationShareTask = stopLiveLocationShareTask, + liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `livedata of live summaries is correctly computed`() { + val entity = LiveLocationShareAggregatedSummaryEntity() + val summary = LiveLocationShareAggregatedSummary( + userId = "", + isActive = true, + endOfLiveTimestampMillis = 123, + lastLocationDataContent = null + ) + + fakeMonarchy.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) + .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) + fakeMonarchy.givenFindAllMappedWithChangesReturns( + realmEntities = listOf(entity), + mappedResult = listOf(summary), + fakeLiveLocationShareAggregatedSummaryMapper + ) + + val result = defaultLocationSharingService.getRunningLiveLocationShareSummaries().value + + result shouldBeEqualTo listOf(summary) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 0a22ef8996..9b4ca332d5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -16,40 +16,62 @@ package org.matrix.android.sdk.test.fakes +import androidx.lifecycle.MutableLiveData import com.zhuinden.monarchy.Monarchy import io.mockk.MockKVerificationScope import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.verify +import io.mockk.slot import io.realm.Realm import io.realm.RealmModel import io.realm.RealmQuery -import io.realm.kotlin.where import org.matrix.android.sdk.internal.util.awaitTransaction internal class FakeMonarchy { val instance = mockk() - private val realm = mockk(relaxed = true) + private val fakeRealm = FakeRealm() init { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { instance.awaitTransaction(any Any>()) } coAnswers { - secondArg Any>().invoke(realm) + secondArg Any>().invoke(fakeRealm.instance) } } - inline fun givenWhereReturns(result: T?) { - val queryResult = mockk>(relaxed = true) - every { queryResult.findFirst() } returns result - every { realm.where() } returns queryResult + inline fun givenWhere(): RealmQuery { + return fakeRealm.givenWhere() + } + + inline fun givenWhereReturns(result: T?): RealmQuery { + return fakeRealm.givenWhere() + .givenFindFirst(result) } inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { - verify { realm.insertOrUpdate(verification()) } + fakeRealm.verifyInsertOrUpdate(verification) + } + + inline fun givenFindAllMappedWithChangesReturns( + realmEntities: List, + mappedResult: List, + mapper: Monarchy.Mapper + ) { + every { mapper.map(any()) } returns mockk() + val monarchyQuery = slot>() + val monarchyMapper = slot>() + every { + instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper)) + } answers { + monarchyQuery.captured.createQuery(fakeRealm.instance) + realmEntities.forEach { + monarchyMapper.captured.map(it) + } + MutableLiveData(mappedResult) + } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index 1697921a8d..d60c9a627e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.test.fakes +import io.mockk.MockKVerificationScope import io.mockk.every import io.mockk.mockk +import io.mockk.verify import io.realm.Realm import io.realm.RealmModel import io.realm.RealmQuery @@ -33,6 +35,67 @@ internal class FakeRealm { every { instance.where() } returns query return query } + + inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { + verify { instance.insertOrUpdate(verification()) } + } +} + +inline fun RealmQuery.givenFindFirst( + result: T? +): RealmQuery { + every { findFirst() } returns result + return this +} + +inline fun RealmQuery.givenFindAll( + result: List +): RealmQuery { + val realmResults = mockk>() + result.forEachIndexed { index, t -> + every { realmResults[index] } returns t + } + every { realmResults.size } returns result.size + every { findAll() } returns realmResults + return this +} + +inline fun RealmQuery.givenEqualTo( + fieldName: String, + value: String +): RealmQuery { + every { equalTo(fieldName, value) } returns this + return this +} + +inline fun RealmQuery.givenEqualTo( + fieldName: String, + value: Boolean +): RealmQuery { + every { equalTo(fieldName, value) } returns this + return this +} + +inline fun RealmQuery.givenNotEqualTo( + fieldName: String, + value: String +): RealmQuery { + every { notEqualTo(fieldName, value) } returns this + return this +} + +inline fun RealmQuery.givenIsNotEmpty( + fieldName: String +): RealmQuery { + every { isNotEmpty(fieldName) } returns this + return this +} + +inline fun RealmQuery.givenIsNotNull( + fieldName: String +): RealmQuery { + every { isNotNull(fieldName) } returns this + return this } inline fun RealmQuery.givenFindFirst( From 7332c08bb408965f509c9e3ec424944932cf0d91 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 17:07:39 +0200 Subject: [PATCH 172/314] Unit test for static location sending --- .../DefaultLocationSharingServiceTest.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index 42db5761d7..c816cd4d01 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -16,12 +16,17 @@ package org.matrix.android.sdk.internal.session.room.location +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields @@ -31,7 +36,11 @@ import org.matrix.android.sdk.test.fakes.givenIsNotEmpty import org.matrix.android.sdk.test.fakes.givenIsNotNull private const val A_ROOM_ID = "room_id" +private const val A_LATITUDE = 1.4 +private const val A_LONGITUDE = 40.0 +private const val AN_UNCERTAINTY = 5.0 +@ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { private val fakeRoomId = A_ROOM_ID @@ -57,6 +66,42 @@ internal class DefaultLocationSharingServiceTest { unmockkAll() } + @Test + fun `static location can be sent`() = runTest { + val isUserLocation = true + val cancelable = mockk() + coEvery { sendStaticLocationTask.execute(any()) } returns cancelable + + val result = defaultLocationSharingService.sendStaticLocation( + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = AN_UNCERTAINTY, + isUserLocation = isUserLocation + ) + + result shouldBeEqualTo cancelable + val expectedParams = SendStaticLocationTask.Params( + roomId = A_ROOM_ID, + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = AN_UNCERTAINTY, + isUserLocation = isUserLocation, + ) + coVerify { sendStaticLocationTask.execute(expectedParams) } + } + + @Test + fun `live location can be sent`() { + } + + @Test + fun `live location share can be started with a given timeout`() { + } + + @Test + fun `live location share can be stopped`() { + } + @Test fun `livedata of live summaries is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() From fb7fbced3985d47a421feb96a59bb4a31067eff2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 17:11:47 +0200 Subject: [PATCH 173/314] Unit test for live location sending --- .../DefaultLocationSharingServiceTest.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index c816cd4d01..fe37383b5e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.test.fakes.givenIsNotEmpty import org.matrix.android.sdk.test.fakes.givenIsNotNull private const val A_ROOM_ID = "room_id" +private const val AN_EVENT_ID = "event_id" private const val A_LATITUDE = 1.4 private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 @@ -91,7 +92,26 @@ internal class DefaultLocationSharingServiceTest { } @Test - fun `live location can be sent`() { + fun `live location can be sent`() = runTest { + val cancelable = mockk() + coEvery { sendLiveLocationTask.execute(any()) } returns cancelable + + val result = defaultLocationSharingService.sendLiveLocation( + beaconInfoEventId = AN_EVENT_ID, + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = AN_UNCERTAINTY + ) + + result shouldBeEqualTo cancelable + val expectedParams = SendLiveLocationTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = AN_UNCERTAINTY + ) + coVerify { sendLiveLocationTask.execute(expectedParams) } } @Test From f981900cf31798490cd207c96b16e1a7676a9c13 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 17:22:04 +0200 Subject: [PATCH 174/314] Unit test for start/stop live location share --- .../DefaultLocationSharingServiceTest.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index fe37383b5e..003842be28 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -18,7 +18,9 @@ package org.matrix.android.sdk.internal.session.room.location import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -40,6 +42,7 @@ private const val AN_EVENT_ID = "event_id" private const val A_LATITUDE = 1.4 private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 +private const val A_TIMEOUT = 15_000L @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { @@ -115,11 +118,29 @@ internal class DefaultLocationSharingServiceTest { } @Test - fun `live location share can be started with a given timeout`() { + fun `live location share can be started with a given timeout`() = runTest { + coEvery { startLiveLocationShareTask.execute(any()) } returns AN_EVENT_ID + + val eventId = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + + eventId shouldBeEqualTo AN_EVENT_ID + val expectedParams = StartLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + timeoutMillis = A_TIMEOUT + ) + coVerify { startLiveLocationShareTask.execute(expectedParams) } } @Test - fun `live location share can be stopped`() { + fun `live location share can be stopped`() = runTest { + coEvery { stopLiveLocationShareTask.execute(any()) } just runs + + defaultLocationSharingService.stopLiveLocationShare() + + val expectedParams = StopLiveLocationShareTask.Params( + roomId = A_ROOM_ID + ) + coVerify { stopLiveLocationShareTask.execute(expectedParams) } } @Test From 1ecc42c903ccf43c52246fcfdbc048ba88ecba3f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 13 Jun 2022 17:40:35 +0200 Subject: [PATCH 175/314] Unit test for send static location task --- .../room/location/SendStaticLocationTask.kt | 1 - .../DefaultSendStaticLocationTaskTest.kt | 73 +++++++++++++++++++ .../test/fakes/FakeEventSenderProcessor.kt | 27 +++++++ .../test/fakes/FakeLocalEchoEventFactory.kt | 70 ++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt index d26609196f..65b11472ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt @@ -32,7 +32,6 @@ internal interface SendStaticLocationTask : Task() + + fun givenCreateLocationEvent(withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createLocationEvent( + roomId = any(), + latitude = any(), + longitude = any(), + uncertainty = any(), + isUserLocation = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + + fun verifyCreateLocationEvent( + roomId: String, + latitude: Double, + longitude: Double, + uncertainty: Double?, + isUserLocation: Boolean + ) { + verify { + instance.createLocationEvent( + roomId = roomId, + latitude = latitude, + longitude = longitude, + uncertainty = uncertainty, + isUserLocation = isUserLocation + ) + } + } + + fun verifyCreateLocalEcho(event: Event) { + verify { instance.createLocalEcho(event) } + } +} From 879cafc8d1f873db0e66abcde4d02a64df8d2cb0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 14 Jun 2022 11:04:57 +0200 Subject: [PATCH 176/314] Unit test for send live location task --- .../room/location/SendLiveLocationTask.kt | 1 - .../DefaultSendLiveLocationTaskTest.kt | 74 +++++++++++++++++++ .../test/fakes/FakeLocalEchoEventFactory.kt | 36 +++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt index ed40c82b66..bebd9c774a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt @@ -32,7 +32,6 @@ internal interface SendLiveLocationTask : Task Date: Tue, 14 Jun 2022 11:06:17 +0200 Subject: [PATCH 177/314] Renaming method to create static location event --- .../session/room/location/SendStaticLocationTask.kt | 2 +- .../internal/session/room/send/LocalEchoEventFactory.kt | 2 +- .../room/location/DefaultSendStaticLocationTaskTest.kt | 4 ++-- .../android/sdk/test/fakes/FakeLocalEchoEventFactory.kt | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt index 65b11472ea..e08b82f3d4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt @@ -38,7 +38,7 @@ internal class DefaultSendStaticLocationTask @Inject constructor( ) : SendStaticLocationTask { override suspend fun execute(params: SendStaticLocationTask.Params): Cancelable { - val event = localEchoEventFactory.createLocationEvent( + val event = localEchoEventFactory.createStaticLocationEvent( roomId = params.roomId, latitude = params.latitude, longitude = params.longitude, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 3b9ca44d18..bcaa257d78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun createLocationEvent( + fun createStaticLocationEvent( roomId: String, latitude: Double, longitude: Double, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt index 440d27657e..3a09ea51c1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt @@ -55,13 +55,13 @@ internal class DefaultSendStaticLocationTaskTest { isUserLocation = true ) - val event = fakeLocalEchoEventFactory.givenCreateLocationEvent( + val event = fakeLocalEchoEventFactory.givenCreateStaticLocationEvent( withLocalEcho = true ) defaultSendStaticLocationTask.execute(params) - fakeLocalEchoEventFactory.verifyCreateLocationEvent( + fakeLocalEchoEventFactory.verifyCreateStaticLocationEvent( roomId = params.roomId, latitude = params.latitude, longitude = params.longitude, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt index 72fc1cc97e..50ec85f14a 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt @@ -28,10 +28,10 @@ internal class FakeLocalEchoEventFactory { val instance = mockk() - fun givenCreateLocationEvent(withLocalEcho: Boolean): Event { + fun givenCreateStaticLocationEvent(withLocalEcho: Boolean): Event { val event = Event() every { - instance.createLocationEvent( + instance.createStaticLocationEvent( roomId = any(), latitude = any(), longitude = any(), @@ -64,7 +64,7 @@ internal class FakeLocalEchoEventFactory { return event } - fun verifyCreateLocationEvent( + fun verifyCreateStaticLocationEvent( roomId: String, latitude: Double, longitude: Double, @@ -72,7 +72,7 @@ internal class FakeLocalEchoEventFactory { isUserLocation: Boolean ) { verify { - instance.createLocationEvent( + instance.createStaticLocationEvent( roomId = roomId, latitude = latitude, longitude = longitude, From 8d2a914c64482cc58a03fd7b0f76eb4d8fdf47e2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 14 Jun 2022 11:18:47 +0200 Subject: [PATCH 178/314] Wip --- .../location/StartLiveLocationShareTask.kt | 1 - .../DefaultStartLiveLocationShareTaskTest.kt | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index c13f625a41..7da67d7539 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -32,7 +32,6 @@ internal interface StartLiveLocationShareTask : Task Date: Tue, 14 Jun 2022 14:30:24 +0200 Subject: [PATCH 179/314] Improving send locations tasks tests --- .../room/location/DefaultSendLiveLocationTaskTest.kt | 9 +++++++-- .../room/location/DefaultSendStaticLocationTaskTest.kt | 9 +++++++-- .../android/sdk/test/fakes/FakeEventSenderProcessor.kt | 5 ++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt index 0cc3f29b52..423c680054 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt @@ -16,11 +16,14 @@ package org.matrix.android.sdk.internal.session.room.location +import io.mockk.mockk import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory @@ -55,13 +58,15 @@ internal class DefaultSendLiveLocationTaskTest { longitude = A_LONGITUDE, uncertainty = AN_UNCERTAINTY ) - val event = fakeLocalEchoEventFactory.givenCreateLiveLocationEvent( withLocalEcho = true ) + val cancelable = mockk() + fakeEventSenderProcessor.givenPostEventReturns(event, cancelable) - defaultSendLiveLocationTask.execute(params) + val result = defaultSendLiveLocationTask.execute(params) + result shouldBeEqualTo cancelable fakeLocalEchoEventFactory.verifyCreateLiveLocationEvent( roomId = params.roomId, beaconInfoEventId = params.beaconInfoEventId, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt index 3a09ea51c1..cfde568b71 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt @@ -16,11 +16,14 @@ package org.matrix.android.sdk.internal.session.room.location +import io.mockk.mockk import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory @@ -54,13 +57,15 @@ internal class DefaultSendStaticLocationTaskTest { uncertainty = AN_UNCERTAINTY, isUserLocation = true ) - val event = fakeLocalEchoEventFactory.givenCreateStaticLocationEvent( withLocalEcho = true ) + val cancelable = mockk() + fakeEventSenderProcessor.givenPostEventReturns(event, cancelable) - defaultSendStaticLocationTask.execute(params) + val result = defaultSendStaticLocationTask.execute(params) + result shouldBeEqualTo cancelable fakeLocalEchoEventFactory.verifyCreateStaticLocationEvent( roomId = params.roomId, latitude = params.latitude, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt index 1f6938d1dd..fbdcf5bfd7 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.test.fakes +import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable @@ -23,5 +24,7 @@ import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProces internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() { - override fun postEvent(event: Event): Cancelable = mockk() + fun givenPostEventReturns(event: Event, cancelable: Cancelable) { + every { postEvent(event) } returns cancelable + } } From af039371e1ee7adbf03f9cff7c763b0c930b4dec Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 14 Jun 2022 14:47:08 +0200 Subject: [PATCH 180/314] Adding test for start live location share task --- .../DefaultStartLiveLocationShareTaskTest.kt | 63 ++++++++++++++++++- .../sdk/test/fakes/FakeSendStateTask.kt | 33 ++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index fe96fdd902..c435e60db3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -16,9 +16,68 @@ package org.matrix.android.sdk.internal.session.room.location +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeSendStateTask + private const val A_USER_ID = "user-id" +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" +private const val A_TIMEOUT = 15_000L +private const val AN_EPOCH = 1655210176L -class DefaultStartLiveLocationShareTaskTest { +@ExperimentalCoroutinesApi +internal class DefaultStartLiveLocationShareTaskTest { - private val fakeClock = FakeClock + private val fakeClock = FakeClock() + private val fakeSendStateTask = FakeSendStateTask() + + private val defaultStartLiveLocationShareTask = DefaultStartLiveLocationShareTask( + userId = A_USER_ID, + clock = fakeClock, + sendStateTask = fakeSendStateTask + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters when calling the task then it is correctly executed`() = runTest { + val params = StartLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + timeoutMillis = A_TIMEOUT + ) + fakeClock.givenEpoch(AN_EPOCH) + fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) + + val result = defaultStartLiveLocationShareTask.execute(params) + + result shouldBeEqualTo AN_EVENT_ID + val expectedBeaconContent = MessageBeaconInfoContent( + timeout = params.timeoutMillis, + isLive = true, + unstableTimestampMillis = AN_EPOCH + ).toContent() + val expectedParams = SendStateTask.Params( + roomId = params.roomId, + stateKey = A_USER_ID, + eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + body = expectedBeaconContent + ) + fakeSendStateTask.verifyExecuteRetry( + params = expectedParams, + remainingRetry = 3 + ) + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt new file mode 100644 index 0000000000..0999ba619b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt @@ -0,0 +1,33 @@ +/* + * 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.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.state.SendStateTask + +internal class FakeSendStateTask : SendStateTask by mockk() { + + fun givenExecuteRetryReturns(eventId: String) { + coEvery { executeRetry(any(), any()) } returns eventId + } + + fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) { + coVerify { executeRetry(params, remainingRetry) } + } +} From d0b598463f8747ff66a07b47a7d41e9bb428900b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 14 Jun 2022 15:16:06 +0200 Subject: [PATCH 181/314] Adding test for stop live location share task --- .../location/StopLiveLocationShareTask.kt | 1 - .../DefaultStopLiveLocationShareTaskTest.kt | 93 +++++++++++++++++++ .../test/fakes/FakeStateEventDataSource.kt | 49 ++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index 44712e75d9..1c282684a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -35,7 +35,6 @@ internal interface StopLiveLocationShareTask : Task Date: Tue, 14 Jun 2022 15:21:51 +0200 Subject: [PATCH 182/314] Fixing pusherTask tests --- .../internal/session/pushers/DefaultAddPusherTaskTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt index 32b1d44fb9..dac33069f3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt @@ -23,10 +23,12 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.model.PusherEntityFields import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakePushersAPI import org.matrix.android.sdk.test.fakes.FakeRequestExecutor +import org.matrix.android.sdk.test.fakes.givenEqualTo import java.net.SocketException private val A_JSON_PUSHER = JsonPusher( @@ -56,6 +58,7 @@ class DefaultAddPusherTaskTest { @Test fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() { monarchy.givenWhereReturns(result = null) + .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } @@ -71,6 +74,7 @@ class DefaultAddPusherTaskTest { fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() { val realmResult = PusherEntity(appDisplayName = null) monarchy.givenWhereReturns(result = realmResult) + .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } @@ -84,6 +88,7 @@ class DefaultAddPusherTaskTest { fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() { val realmResult = PusherEntity() monarchy.givenWhereReturns(result = realmResult) + .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) pushersAPI.givenSetPusherErrors(SocketException()) assertFailsWith { @@ -96,6 +101,7 @@ class DefaultAddPusherTaskTest { @Test fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() { monarchy.givenWhereReturns(result = null) + .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) pushersAPI.givenSetPusherErrors(SocketException()) assertFailsWith { From b16ccf5098d917dac7291c6d3470cf6a16bb2274 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 10:16:27 +0200 Subject: [PATCH 183/314] Fix unit tests after rebase --- .../DefaultStopLiveLocationShareTaskTest.kt | 3 +- .../android/sdk/test/fakes/FakeRealm.kt | 43 ------------------- .../test/fakes/FakeStateEventDataSource.kt | 4 +- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 55d13803b9..81a5742f90 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent @@ -87,7 +86,7 @@ class DefaultStopLiveLocationShareTaskTest { fakeStateEventDataSource.verifyGetStateEvent( roomId = params.roomId, eventType = EventType.STATE_ROOM_BEACON_INFO.first(), - stateKey = QueryStringValue.Equals(A_USER_ID) + stateKey = A_USER_ID ) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index d60c9a627e..0ebff87278 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -97,46 +97,3 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } returns this return this } - -inline fun RealmQuery.givenFindFirst( - result: T? -): RealmQuery { - every { findFirst() } returns result - return this -} - -inline fun RealmQuery.givenFindAll( - result: List -): RealmQuery { - val realmResults = mockk>() - result.forEachIndexed { index, t -> - every { realmResults[index] } returns t - } - every { realmResults.size } returns result.size - every { findAll() } returns realmResults - return this -} - -inline fun RealmQuery.givenEqualTo( - fieldName: String, - value: String -): RealmQuery { - every { equalTo(fieldName, value) } returns this - return this -} - -inline fun RealmQuery.givenEqualTo( - fieldName: String, - value: Boolean -): RealmQuery { - every { equalTo(fieldName, value) } returns this - return this -} - -inline fun RealmQuery.givenNotEqualTo( - fieldName: String, - value: String -): RealmQuery { - every { notEqualTo(fieldName, value) } returns this - return this -} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt index e4f19abaa7..498901bdac 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt @@ -37,12 +37,12 @@ internal class FakeStateEventDataSource { } returns event } - fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue) { + fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) { verify { instance.getStateEvent( roomId = roomId, eventType = eventType, - stateKey = stateKey + stateKey = QueryStringValue.Equals(stateKey) ) } } From e55c378683f5026055051ce6eb80fa0e32fb2c11 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 10:01:35 +0200 Subject: [PATCH 184/314] Catching crash when offline during start of a live location share --- .../room/location/LocationSharingService.kt | 4 +-- .../location/UpdateLiveLocationShareResult.kt | 32 +++++++++++++++++++ .../location/DefaultLocationSharingService.kt | 3 +- .../location/StartLiveLocationShareTask.kt | 17 ++++++++-- .../location/StopLiveLocationShareTask.kt | 1 + .../location/LocationSharingService.kt | 28 +++++++++++----- 6 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 11b74ecd7f..ce0b746d4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -46,9 +46,9 @@ interface LocationSharingService { /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds - * @return the id of the created beacon info event + * @return the result of the update of the live */ - suspend fun startLiveLocationShare(timeoutMillis: Long): String + suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult /** * Stops sharing live location in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt new file mode 100644 index 0000000000..6c6bc1029a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt @@ -0,0 +1,32 @@ +/* + * 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 org.matrix.android.sdk.api.session.room.location + +/** + * Represents the result of an update of live location share like a start or a stop. + */ +sealed interface UpdateLiveLocationShareResult { + /** + * @param beaconEventId event id of the updated state event + */ + data class Success(val beaconEventId: String) : UpdateLiveLocationShareResult + + /** + * @param error thrown during the update + */ + data class Failure(val error: Throwable) : UpdateLiveLocationShareResult +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 3fa00fa077..c15cdda6f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -22,6 +22,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.location.LocationSharingService +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper @@ -66,7 +67,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return sendLiveLocationTask.execute(params) } - override suspend fun startLiveLocationShare(timeoutMillis: Long): String { + override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { val params = StartLiveLocationShareTask.Params( roomId = roomId, timeoutMillis = timeoutMillis diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index 7da67d7539..bf6a0049d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.location import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.state.SendStateTask @@ -25,20 +26,21 @@ import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject -internal interface StartLiveLocationShareTask : Task { +internal interface StartLiveLocationShareTask : Task { data class Params( val roomId: String, val timeoutMillis: Long, ) } +// TODO update unit test internal class DefaultStartLiveLocationShareTask @Inject constructor( @UserId private val userId: String, private val clock: Clock, private val sendStateTask: SendStateTask, ) : StartLiveLocationShareTask { - override suspend fun execute(params: StartLiveLocationShareTask.Params): String { + override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult { val beaconContent = MessageBeaconInfoContent( timeout = params.timeoutMillis, isLive = true, @@ -51,6 +53,15 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor( eventType = eventType, body = beaconContent ) - return sendStateTask.executeRetry(sendStateTaskParams, 3) + return try { + val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3) + if (eventId.isNotEmpty()) { + UpdateLiveLocationShareResult.Success(eventId) + } else { + UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event")) + } + } catch (error: Throwable) { + UpdateLiveLocationShareResult.Failure(error) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index 1c282684a4..8f2fa27288 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -41,6 +41,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor( private val stateEventDataSource: StateEventDataSource, ) : StopLiveLocationShareTask { + @Throws override suspend fun execute(params: StopLiveLocationShareTask.Params) { val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return val stateKey = beaconInfoStateEvent.stateKey ?: return diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 77f3abcc28..7df84a4cee 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import timber.log.Timber import java.util.Timer import java.util.TimerTask @@ -95,13 +96,20 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis) beaconEventId - ?.takeUnless { it.isEmpty() } - ?.let { - roomArgsMap[it] = roomArgs - locationTracker.requestLastKnownLocation() + ?.let { result -> + when (result) { + is UpdateLiveLocationShareResult.Success -> { + roomArgsMap[result.beaconEventId] = roomArgs + locationTracker.requestLastKnownLocation() + } + is UpdateLiveLocationShareResult.Failure -> { + tryToDestroyMe() + } + } } ?: run { Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id") + tryToDestroyMe() } } @@ -132,10 +140,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { .map { it.key } beaconIds.forEach { roomArgsMap.remove(it) } - if (roomArgsMap.isEmpty()) { - Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") - destroyMe() - } + tryToDestroyMe() } } @@ -178,6 +183,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { stopSelf() } + private fun tryToDestroyMe() { + if (roomArgsMap.isEmpty()) { + Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") + destroyMe() + } + } + private fun destroyMe() { locationTracker.removeCallback(this) timers.forEach { it.cancel() } From 9eba3034db3a3bfa22833520437639bcf43f8f91 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 12:09:39 +0200 Subject: [PATCH 185/314] Catching crash when offline during stop of a live location share --- .../room/location/LocationSharingService.kt | 2 +- .../location/DefaultLocationSharingService.kt | 2 +- .../location/StopLiveLocationShareTask.kt | 27 +++++++++++---- .../location/LocationSharingService.kt | 33 ++++++++++--------- .../LocationSharingServiceConnection.kt | 1 + 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index ce0b746d4f..7e5906b517 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -53,7 +53,7 @@ interface LocationSharingService { /** * Stops sharing live location in the room. */ - suspend fun stopLiveLocationShare() + suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult /** * Returns a LiveData on the list of current running live location shares. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index c15cdda6f3..015c1cca0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -75,7 +75,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return startLiveLocationShareTask.execute(params) } - override suspend fun stopLiveLocationShare() { + override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult { val params = StopLiveLocationShareTask.Params( roomId = roomId, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index 8f2fa27288..dc12054d7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.state.SendStateTask @@ -29,23 +30,23 @@ import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface StopLiveLocationShareTask : Task { +internal interface StopLiveLocationShareTask : Task { data class Params( val roomId: String, ) } +// TODO update unit tests internal class DefaultStopLiveLocationShareTask @Inject constructor( @UserId private val userId: String, private val sendStateTask: SendStateTask, private val stateEventDataSource: StateEventDataSource, ) : StopLiveLocationShareTask { - @Throws - override suspend fun execute(params: StopLiveLocationShareTask.Params) { - val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return - val stateKey = beaconInfoStateEvent.stateKey ?: return - val content = beaconInfoStateEvent.getClearContent()?.toModel() ?: return + override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult { + val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return getResultForIncorrectBeaconInfoEvent() + val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent() + val content = beaconInfoStateEvent.getClearContent()?.toModel() ?: return getResultForIncorrectBeaconInfoEvent() val updatedContent = content.copy(isLive = false).toContent() val sendStateTaskParams = SendStateTask.Params( roomId = params.roomId, @@ -53,9 +54,21 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor( eventType = EventType.STATE_ROOM_BEACON_INFO.first(), body = updatedContent ) - sendStateTask.executeRetry(sendStateTaskParams, 3) + return try { + val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3) + if (eventId.isNotEmpty()) { + UpdateLiveLocationShareResult.Success(eventId) + } else { + UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event")) + } + } catch (error: Throwable) { + UpdateLiveLocationShareResult.Failure(error) + } } + private fun getResultForIncorrectBeaconInfoEvent() = + UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event")) + private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? { return EventType.STATE_ROOM_BEACON_INFO .mapNotNull { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 7df84a4cee..27eea498e4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -131,25 +131,28 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { fun stopSharingLocation(roomId: String) { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") - // Send a new beacon info state by setting live field as false - sendStoppedBeaconInfo(roomId) + launchInIO { session -> + // Send a new beacon info state by setting live field as false + when (sendStoppedBeaconInfo(session, roomId)) { + is UpdateLiveLocationShareResult.Success -> { + synchronized(roomArgsMap) { + val beaconIds = roomArgsMap + .filter { it.value.roomId == roomId } + .map { it.key } + beaconIds.forEach { roomArgsMap.remove(it) } - synchronized(roomArgsMap) { - val beaconIds = roomArgsMap - .filter { it.value.roomId == roomId } - .map { it.key } - beaconIds.forEach { roomArgsMap.remove(it) } - - tryToDestroyMe() + tryToDestroyMe() + } + } + else -> Unit + } } } - private fun sendStoppedBeaconInfo(roomId: String) { - launchInIO { session -> - session.getRoom(roomId) - ?.locationSharingService() - ?.stopLiveLocationShare() - } + private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { + return session.getRoom(roomId) + ?.locationSharingService() + ?.stopLiveLocationShare() } override fun onLocationUpdate(locationData: LocationData) { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index e72f77531b..97f447ec4b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -28,6 +28,7 @@ class LocationSharingServiceConnection @Inject constructor( ) : ServiceConnection { interface Callback { + // TODO add onLocationServiceError() fun onLocationServiceRunning() fun onLocationServiceStopped() } From 31bb9eaac8adf4454d33849f946c7212d9e24012 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 14:34:13 +0200 Subject: [PATCH 186/314] Forward error to UI in timeline screen --- .../features/home/room/detail/TimelineViewModel.kt | 4 ++++ .../features/location/LocationSharingService.kt | 14 ++++++++++---- .../location/LocationSharingServiceConnection.kt | 13 ++++++++++--- .../location/live/map/LocationLiveMapViewModel.kt | 4 ++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 07b20b4914..99a01211c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -1293,6 +1293,10 @@ class TimelineViewModel @AssistedInject constructor( locationSharingServiceConnection.bind(this) } + override fun onLocationServiceError(error: Throwable) { + _viewEvents.post(RoomDetailViewEvents.Failure(error)) + } + override fun onCleared() { timeline.dispose() timeline.removeAllListeners() diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 27eea498e4..4a15a9d643 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -55,8 +55,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { /** * Keep track of a map between beacon event Id starting the live and RoomArgs. */ - private var roomArgsMap = mutableMapOf() - private var timers = mutableListOf() + private val roomArgsMap = mutableMapOf() + private val timers = mutableListOf() + var callback: Callback? = null override fun onCreate() { super.onCreate() @@ -103,6 +104,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { locationTracker.requestLastKnownLocation() } is UpdateLiveLocationShareResult.Failure -> { + callback?.onServiceError(result.error) tryToDestroyMe() } } @@ -132,8 +134,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") launchInIO { session -> - // Send a new beacon info state by setting live field as false - when (sendStoppedBeaconInfo(session, roomId)) { + when (val result = sendStoppedBeaconInfo(session, roomId)) { is UpdateLiveLocationShareResult.Success -> { synchronized(roomArgsMap) { val beaconIds = roomArgsMap @@ -144,6 +145,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { tryToDestroyMe() } } + is UpdateLiveLocationShareResult.Failure -> callback?.onServiceError(result.error) else -> Unit } } @@ -224,6 +226,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { fun getService(): LocationSharingService = this@LocationSharingService } + interface Callback { + fun onServiceError(error: Throwable) + } + companion object { const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS" } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 97f447ec4b..af09e0b1e0 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -25,12 +25,12 @@ import javax.inject.Inject class LocationSharingServiceConnection @Inject constructor( private val context: Context -) : ServiceConnection { +) : ServiceConnection, LocationSharingService.Callback { interface Callback { - // TODO add onLocationServiceError() fun onLocationServiceRunning() fun onLocationServiceStopped() + fun onLocationServiceError(error: Throwable) } private var callback: Callback? = null @@ -58,14 +58,21 @@ class LocationSharingServiceConnection @Inject constructor( } override fun onServiceConnected(className: ComponentName, binder: IBinder) { - locationSharingService = (binder as LocationSharingService.LocalBinder).getService() + locationSharingService = (binder as LocationSharingService.LocalBinder).getService().also { + it.callback = this + } isBound = true callback?.onLocationServiceRunning() } override fun onServiceDisconnected(className: ComponentName) { isBound = false + locationSharingService?.callback = null locationSharingService = null callback?.onLocationServiceStopped() } + + override fun onServiceError(error: Throwable) { + callback?.onLocationServiceError(error) + } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index eb5bccff0f..9ef6449ea0 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -80,4 +80,8 @@ class LocationLiveMapViewModel @AssistedInject constructor( override fun onLocationServiceStopped() { // NOOP } + + override fun onLocationServiceError(error: Throwable) { + // TODO + } } From fc980570424762329efd1b2bc1ed1d03925eeb78 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 14:45:32 +0200 Subject: [PATCH 187/314] Forward error to UI in map screen --- .../app/features/home/room/detail/TimelineViewModel.kt | 2 +- .../location/live/map/LocationLiveMapViewEvents.kt | 4 +++- .../location/live/map/LocationLiveMapViewFragment.kt | 10 ++++++++++ .../location/live/map/LocationLiveMapViewModel.kt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 99a01211c3..1c2255246b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -1294,7 +1294,7 @@ class TimelineViewModel @AssistedInject constructor( } override fun onLocationServiceError(error: Throwable) { - _viewEvents.post(RoomDetailViewEvents.Failure(error)) + _viewEvents.post(RoomDetailViewEvents.Failure(throwable = error, showInDialog = true)) } override fun onCleared() { diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt index 6645ff58d9..23771299c8 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt @@ -18,4 +18,6 @@ package im.vector.app.features.location.live.map import im.vector.app.core.platform.VectorViewEvents -sealed interface LocationLiveMapViewEvents : VectorViewEvents +sealed interface LocationLiveMapViewEvents : VectorViewEvents { + data class Error(val error: Throwable) : LocationLiveMapViewEvents +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 5f2410d697..09522ce4c8 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -76,6 +76,8 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment + when(viewEvent) { + is LocationLiveMapViewEvents.Error -> displayErrorDialog(viewEvent.error) + } + } + } + override fun onResume() { super.onResume() setupMap() diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index 9ef6449ea0..e89649709a 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -82,6 +82,6 @@ class LocationLiveMapViewModel @AssistedInject constructor( } override fun onLocationServiceError(error: Throwable) { - // TODO + _viewEvents.post(LocationLiveMapViewEvents.Error(error)) } } From 6c0b7f7b4337142ace9145f1fd4f25c2bc479f38 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 14:51:28 +0200 Subject: [PATCH 188/314] Renaming a variable to be more precise --- .../im/vector/app/features/location/LocationSharingService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 4a15a9d643..ef612eeec2 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -91,12 +91,12 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { - val beaconEventId = session + val updateLiveResult = session .getRoom(roomArgs.roomId) ?.locationSharingService() ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis) - beaconEventId + updateLiveResult ?.let { result -> when (result) { is UpdateLiveLocationShareResult.Success -> { From 3e05431e6f70bcdfc704814ee5b1bc8a9eb264ba Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 14:56:00 +0200 Subject: [PATCH 189/314] Fixing unit tests --- .../location/DefaultLocationSharingServiceTest.kt | 12 +++++++----- .../DefaultStartLiveLocationShareTaskTest.kt | 5 +++-- .../location/DefaultStopLiveLocationShareTaskTest.kt | 5 ++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index 003842be28..d9b3526bf5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper @@ -119,11 +120,11 @@ internal class DefaultLocationSharingServiceTest { @Test fun `live location share can be started with a given timeout`() = runTest { - coEvery { startLiveLocationShareTask.execute(any()) } returns AN_EVENT_ID + coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val eventId = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) - eventId shouldBeEqualTo AN_EVENT_ID + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT @@ -133,10 +134,11 @@ internal class DefaultLocationSharingServiceTest { @Test fun `live location share can be stopped`() = runTest { - coEvery { stopLiveLocationShareTask.execute(any()) } just runs + coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - defaultLocationSharingService.stopLiveLocationShare() + val result = defaultLocationSharingService.stopLiveLocationShare() + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedParams = StopLiveLocationShareTask.Params( roomId = A_ROOM_ID ) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index c435e60db3..6a487a4e81 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -24,6 +24,7 @@ import org.junit.After import org.junit.Test import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.test.fakes.FakeClock @@ -53,7 +54,7 @@ internal class DefaultStartLiveLocationShareTaskTest { } @Test - fun `given parameters when calling the task then it is correctly executed`() = runTest { + fun `given parameters an no error when calling the task then it is correctly executed`() = runTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT @@ -63,7 +64,7 @@ internal class DefaultStartLiveLocationShareTaskTest { val result = defaultStartLiveLocationShareTask.execute(params) - result shouldBeEqualTo AN_EVENT_ID + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedBeaconContent = MessageBeaconInfoContent( timeout = params.timeoutMillis, isLive = true, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 81a5742f90..78433d8b6e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -19,11 +19,13 @@ package org.matrix.android.sdk.internal.session.room.location import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.test.fakes.FakeSendStateTask @@ -66,8 +68,9 @@ class DefaultStopLiveLocationShareTaskTest { fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) - defaultStopLiveLocationShareTask.execute(params) + val result = defaultStopLiveLocationShareTask.execute(params) + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedBeaconContent = MessageBeaconInfoContent( timeout = A_TIMEOUT, isLive = false, From e1fc6fa727b1964098c6a6493a3260bacce35041 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 15:16:25 +0200 Subject: [PATCH 190/314] Adding tests to cover errors thrown during start/stop process --- .../location/StartLiveLocationShareTask.kt | 1 - .../location/StopLiveLocationShareTask.kt | 1 - .../DefaultStartLiveLocationShareTaskTest.kt | 32 +++++++- .../DefaultStopLiveLocationShareTaskTest.kt | 78 ++++++++++++++++++- .../sdk/test/fakes/FakeSendStateTask.kt | 4 + .../test/fakes/FakeStateEventDataSource.kt | 2 +- 6 files changed, 113 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index bf6a0049d8..b943c27977 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -33,7 +33,6 @@ internal interface StartLiveLocationShareTask : Task + fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) + val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) + + val result = defaultStopLiveLocationShareTask.execute(params) + + result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class + } + } + + @Test + fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest { + val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) + val currentStateEvent = Event( + stateKey = A_USER_ID, + content = MessageBeaconInfoContent( + timeout = A_TIMEOUT, + isLive = true, + unstableTimestampMillis = AN_EPOCH + ).toContent() + ) + fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeSendStateTask.givenExecuteRetryReturns("") + + val result = defaultStopLiveLocationShareTask.execute(params) + + result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class + } + + @Test + fun `given parameters and error during event sending when calling the task then result is failure`() = runTest { + val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) + val currentStateEvent = Event( + stateKey = A_USER_ID, + content = MessageBeaconInfoContent( + timeout = A_TIMEOUT, + isLive = true, + unstableTimestampMillis = AN_EPOCH + ).toContent() + ) + fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + val error = Throwable() + fakeSendStateTask.givenExecuteRetryThrows(error) + + val result = defaultStopLiveLocationShareTask.execute(params) + + result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt index 0999ba619b..08a25be93e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt @@ -27,6 +27,10 @@ internal class FakeSendStateTask : SendStateTask by mockk() { coEvery { executeRetry(any(), any()) } returns eventId } + fun givenExecuteRetryThrows(error: Throwable) { + coEvery { executeRetry(any(), any()) } throws error + } + fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) { coVerify { executeRetry(params, remainingRetry) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt index 498901bdac..ca03316fa7 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt @@ -27,7 +27,7 @@ internal class FakeStateEventDataSource { val instance: StateEventDataSource = mockk() - fun givenGetStateEventReturns(event: Event) { + fun givenGetStateEventReturns(event: Event?) { every { instance.getStateEvent( roomId = any(), From eb503b8ab62641b3600d5d88bcb53bd02d1fce43 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 15:36:08 +0200 Subject: [PATCH 191/314] Adding a changelog entry --- changelog.d/6315.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6315.bugfix diff --git a/changelog.d/6315.bugfix b/changelog.d/6315.bugfix new file mode 100644 index 0000000000..0b5eb6064d --- /dev/null +++ b/changelog.d/6315.bugfix @@ -0,0 +1 @@ +[Location sharing] Fix crash when starting/stopping a live when offline From 082b39e651f23b30cc3bb2515ce51154a02a58f1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 15:48:03 +0200 Subject: [PATCH 192/314] Adding return type in the doc for stop API --- .../sdk/api/session/room/location/LocationSharingService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 7e5906b517..0f88f891cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -52,6 +52,7 @@ interface LocationSharingService { /** * Stops sharing live location in the room. + * @return the result of the update of the live */ suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult From 9047d9d62c562ea28c412bca3f321cf53ed04056 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 15 Jun 2022 16:04:09 +0200 Subject: [PATCH 193/314] Fixing coding style issues --- .../room/location/UpdateLiveLocationShareResult.kt | 9 +-------- .../room/location/DefaultLocationSharingServiceTest.kt | 2 -- .../location/live/map/LocationLiveMapViewFragment.kt | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt index 6c6bc1029a..6f8c03be46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -20,13 +20,6 @@ package org.matrix.android.sdk.api.session.room.location * Represents the result of an update of live location share like a start or a stop. */ sealed interface UpdateLiveLocationShareResult { - /** - * @param beaconEventId event id of the updated state event - */ data class Success(val beaconEventId: String) : UpdateLiveLocationShareResult - - /** - * @param error thrown during the update - */ data class Failure(val error: Throwable) : UpdateLiveLocationShareResult } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index d9b3526bf5..30a9671733 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -18,9 +18,7 @@ package org.matrix.android.sdk.internal.session.room.location import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.just import io.mockk.mockk -import io.mockk.runs import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 09522ce4c8..a57ba74685 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -93,7 +93,7 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment - when(viewEvent) { + when (viewEvent) { is LocationLiveMapViewEvents.Error -> displayErrorDialog(viewEvent.error) } } From 65bc4acbabf6c315160b61c8bf5f6947b7307b9c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 20 Jun 2022 11:23:02 +0200 Subject: [PATCH 194/314] Fix flaky tests for voice recording feature (#6330) --- changelog.d/6329.misc | 1 + .../im/vector/app/core/utils/WaitUntil.kt | 69 +++++++++++++++++++ .../app/features/voice/VoiceRecorderLTests.kt | 8 +-- .../app/features/voice/VoiceRecorderQTests.kt | 51 +++++--------- .../features/voice/VoiceRecorderTestExt.kt | 36 ++++++++++ .../features/voice/AbstractVoiceRecorder.kt | 3 +- 6 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 changelog.d/6329.misc create mode 100644 vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt create mode 100644 vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt diff --git a/changelog.d/6329.misc b/changelog.d/6329.misc new file mode 100644 index 0000000000..dd87c11f6e --- /dev/null +++ b/changelog.d/6329.misc @@ -0,0 +1 @@ +Fix flaky test in voice recording feature. diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt b/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt new file mode 100644 index 0000000000..16abada04c --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt @@ -0,0 +1,69 @@ +/* + * 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.utils + +import kotlinx.coroutines.delay +import org.amshove.kluent.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * Tries a [condition] several times until it returns true or a [timeout] is reached waiting for some [retryDelay] time between retries. + * On timeout it fails with an [errorMessage]. + */ +suspend fun waitUntilCondition( + errorMessage: String, + timeout: Duration = 1.seconds, + retryDelay: Duration = 50.milliseconds, + condition: () -> Boolean, +) { + val start = System.currentTimeMillis() + do { + if (condition()) return + delay(retryDelay.inWholeMilliseconds) + } while (System.currentTimeMillis() - start < timeout.inWholeMilliseconds) + fail(errorMessage) +} + +/** + * Tries a [block] several times until it runs with no errors or a [timeout] is reached waiting for some [retryDelay] time between retries. + * On timeout it fails with a custom [errorMessage] or a caught [AssertionError]. + */ +suspend fun waitUntil( + errorMessage: String? = null, + timeout: Duration = 1.seconds, + retryDelay: Duration = 50.milliseconds, + block: () -> Unit, +) { + var error: AssertionError? + val start = System.currentTimeMillis() + do { + try { + block() + return + } catch (e: AssertionError) { + error = e + } + delay(retryDelay.inWholeMilliseconds) + } while (System.currentTimeMillis() - start < timeout.inWholeMilliseconds) + if (errorMessage != null) { + fail(errorMessage) + } else { + throw error!! + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt index c02c2cac80..1687ee4388 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt @@ -21,6 +21,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.mockk.spyk import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldExist import org.amshove.kluent.shouldNotBeNull @@ -42,8 +43,7 @@ class VoiceRecorderLTests { getVoiceMessageFile().shouldBeNull() startRecord("some_room_id") - - getVoiceMessageFile().shouldNotBeNullAndExist() + runBlocking { waitUntilRecordingFileExists() } stopRecord() } @@ -53,6 +53,7 @@ class VoiceRecorderLTests { getVoiceMessageFile().shouldBeNull() startRecord("some_room_id") + runBlocking { waitUntilRecordingFileExists() } stopRecord() getVoiceMessageFile().shouldNotBeNullAndExist() @@ -61,8 +62,7 @@ class VoiceRecorderLTests { @Test fun cancelRecordRemovesFile() = with(recorder) { startRecord("some_room_id") - val file = recorder.getVoiceMessageFile() - file.shouldNotBeNullAndExist() + val file = runBlocking { waitUntilRecordingFileExists() } cancelRecord() diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt index 446d9e5b21..b9dc3f6d41 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderQTests.kt @@ -23,7 +23,6 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldExist @@ -43,50 +42,36 @@ class VoiceRecorderQTests { private val recorder = spyk(VoiceRecorderQ(context)) @Test - fun startRecordCreatesOggFile() = runBlocking { - with(recorder) { - getVoiceMessageFile().shouldBeNull() + fun startRecordCreatesOggFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() - startRecord("some_room_id") - waitForRecording() + startRecord("some_room_id") + runBlocking { waitUntilRecordingFileExists() } - getVoiceMessageFile().shouldNotBeNullAndExist() - - stopRecord() - } + stopRecord() } @Test - fun stopRecordKeepsFile() = runBlocking { - with(recorder) { - getVoiceMessageFile().shouldBeNull() + fun stopRecordKeepsFile() = with(recorder) { + getVoiceMessageFile().shouldBeNull() - startRecord("some_room_id") - waitForRecording() - stopRecord() + startRecord("some_room_id") + runBlocking { waitUntilRecordingFileExists() } + stopRecord() - getVoiceMessageFile().shouldNotBeNullAndExist() - } + getVoiceMessageFile().shouldNotBeNullAndExist() } @Test - fun cancelRecordRemovesFileAfterStopping() = runBlocking { - with(recorder) { - startRecord("some_room_id") - val file = recorder.getVoiceMessageFile() - file.shouldNotBeNullAndExist() + fun cancelRecordRemovesFileAfterStopping() = with(recorder) { + startRecord("some_room_id") + val file = runBlocking { waitUntilRecordingFileExists() } + cancelRecord() - waitForRecording() - cancelRecord() - - verify { stopRecord() } - getVoiceMessageFile().shouldBeNull() - file!!.shouldNotExist() - } + verify { stopRecord() } + getVoiceMessageFile().shouldBeNull() + file!!.shouldNotExist() } - - // Give MediaRecorder some time to actually start recording - private suspend fun waitForRecording() = delay(10) } private fun File?.shouldNotBeNullAndExist() { diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt new file mode 100644 index 0000000000..4275ae89b3 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt @@ -0,0 +1,36 @@ +/* + * 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.voice + +import im.vector.app.core.utils.waitUntil +import org.amshove.kluent.shouldExist +import org.amshove.kluent.shouldNotBeNull +import java.io.File +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +// Give voice recorders some time to start recording and create the audio file +suspend fun VoiceRecorder.waitUntilRecordingFileExists(timeout: Duration = 1.seconds, delay: Duration = 10.milliseconds): File? { + waitUntil(timeout = timeout, retryDelay = delay) { + getVoiceMessageFile().run { + shouldNotBeNull() + shouldExist() + } + } + return getVoiceMessageFile() +} diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt index 91eb371f42..5e27aa5bb2 100644 --- a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -19,6 +19,7 @@ package im.vector.app.features.voice import android.content.Context import android.media.MediaRecorder import android.os.Build +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.util.md5 import java.io.File @@ -80,7 +81,7 @@ abstract class AbstractVoiceRecorder( override fun stopRecord() { // Can throw when the record is less than 1 second. mediaRecorder?.let { - it.stop() + tryOrNull { it.stop() } it.reset() it.release() } From 9641ff132d520d54306a333cf31c1b26f08fe7a1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 20 Jun 2022 13:58:28 +0300 Subject: [PATCH 195/314] Show live location sharing option even if labs flag is disabled. --- .../app/features/location/LocationSharingFragment.kt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 1b25f3fcec..e25520ed81 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -41,7 +41,6 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.location.live.duration.ChooseLiveDurationBottomSheet import im.vector.app.features.location.option.LocationSharingOption -import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.util.MatrixItem import java.lang.ref.WeakReference import javax.inject.Inject @@ -53,7 +52,6 @@ class LocationSharingFragment @Inject constructor( private val urlMapProvider: UrlMapProvider, private val avatarRenderer: AvatarRenderer, private val matrixItemColorProvider: MatrixItemColorProvider, - private val vectorPreferences: VectorPreferences, ) : VectorBaseFragment(), LocationTargetChangeListener, VectorBaseBottomSheetDialogFragment.ResultListener { @@ -223,13 +221,7 @@ class LocationSharingFragment @Inject constructor( private fun updateMap(state: LocationSharingViewState) { // first, update the options view val options: Set = when (state.areTargetAndUserLocationEqual) { - true -> { - if (vectorPreferences.labsEnableLiveLocation()) { - setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) - } else { - setOf(LocationSharingOption.USER_CURRENT) - } - } + true -> setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) false -> setOf(LocationSharingOption.PINNED) else -> emptySet() } From 7ddec674fb0ba587f106e745f0b6bcac4e6c2faa Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 10:16:27 +0200 Subject: [PATCH 196/314] Fixing unit tests after rebase --- .../DefaultStopLiveLocationShareTaskTest.kt | 3 +- .../android/sdk/test/fakes/FakeRealm.kt | 43 ------------------- .../test/fakes/FakeStateEventDataSource.kt | 4 +- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 55d13803b9..81a5742f90 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent @@ -87,7 +86,7 @@ class DefaultStopLiveLocationShareTaskTest { fakeStateEventDataSource.verifyGetStateEvent( roomId = params.roomId, eventType = EventType.STATE_ROOM_BEACON_INFO.first(), - stateKey = QueryStringValue.Equals(A_USER_ID) + stateKey = A_USER_ID ) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index d60c9a627e..0ebff87278 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -97,46 +97,3 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } returns this return this } - -inline fun RealmQuery.givenFindFirst( - result: T? -): RealmQuery { - every { findFirst() } returns result - return this -} - -inline fun RealmQuery.givenFindAll( - result: List -): RealmQuery { - val realmResults = mockk>() - result.forEachIndexed { index, t -> - every { realmResults[index] } returns t - } - every { realmResults.size } returns result.size - every { findAll() } returns realmResults - return this -} - -inline fun RealmQuery.givenEqualTo( - fieldName: String, - value: String -): RealmQuery { - every { equalTo(fieldName, value) } returns this - return this -} - -inline fun RealmQuery.givenEqualTo( - fieldName: String, - value: Boolean -): RealmQuery { - every { equalTo(fieldName, value) } returns this - return this -} - -inline fun RealmQuery.givenNotEqualTo( - fieldName: String, - value: String -): RealmQuery { - every { notEqualTo(fieldName, value) } returns this - return this -} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt index e4f19abaa7..498901bdac 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt @@ -37,12 +37,12 @@ internal class FakeStateEventDataSource { } returns event } - fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue) { + fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) { verify { instance.getStateEvent( roomId = roomId, eventType = eventType, - stateKey = stateKey + stateKey = QueryStringValue.Equals(stateKey) ) } } From b37dce7da7aa4df47d46fa27f343e429d3acdacf Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 20 Jun 2022 15:19:22 +0300 Subject: [PATCH 197/314] Create layout for promoting live location labs flag. --- ...heet_live_location_labs_flag_promotion.xml | 56 +++++++++++++++++++ vector/src/main/res/values/strings.xml | 6 ++ 2 files changed, 62 insertions(+) create mode 100644 vector/src/main/res/layout/bottom_sheet_live_location_labs_flag_promotion.xml diff --git a/vector/src/main/res/layout/bottom_sheet_live_location_labs_flag_promotion.xml b/vector/src/main/res/layout/bottom_sheet_live_location_labs_flag_promotion.xml new file mode 100644 index 0000000000..438e60a4d3 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_live_location_labs_flag_promotion.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + +