Merge remote-tracking branch 'origin/develop' into task/eric/space-switching-unit-tests

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
This commit is contained in:
ericdecanini 2022-07-28 11:25:14 +02:00
commit e6addd1319
181 changed files with 3136 additions and 602 deletions

View File

@ -37,7 +37,7 @@ jobs:
mv towncrier.toml towncrier.toml.bak mv towncrier.toml towncrier.toml.bak
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak rm towncrier.toml.bak
yes n | towncrier --version nightly yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly APK - name: Build and upload Gplay Nightly APK
run: | run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES --stacktrace ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES --stacktrace

View File

@ -248,9 +248,12 @@ jobs:
# Skip in forks # Skip in forks
if: > if: >
github.repository == 'vector-im/element-android' && github.repository == 'vector-im/element-android' &&
(contains(github.event.issue.labels.*.name, 'Z-ElementX-Alpha') || (contains(github.event.issue.labels.*.name, 'Z-BBQ-Alpha') ||
contains(github.event.issue.labels.*.name, 'Z-ElementX-Beta') || contains(github.event.issue.labels.*.name, 'Z-BBQ-Beta') ||
contains(github.event.issue.labels.*.name, 'Z-ElementX')) contains(github.event.issue.labels.*.name, 'Z-BBQ-Release') ||
contains(github.event.issue.labels.*.name, 'Z-Banquet-Alpha') ||
contains(github.event.issue.labels.*.name, 'Z-Banquet-Beta') ||
contains(github.event.issue.labels.*.name, 'Z-Banquet-Release'))
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:

1
changelog.d/2585.feature Normal file
View File

@ -0,0 +1 @@
FTUE - Enable improved login and register onboarding flows

1
changelog.d/5115.bugfix Normal file
View File

@ -0,0 +1 @@
Stop using unstable names for withheld codes

1
changelog.d/6314.misc Normal file
View File

@ -0,0 +1 @@
Improves performance on search screen by replacing flattenParents with directParentName in RoomSummary

1
changelog.d/6341.bugfix Normal file
View File

@ -0,0 +1 @@
Fixed issues with reporting sync state events from different threads

1
changelog.d/6395.bugfix Normal file
View File

@ -0,0 +1 @@
Display specific message when verification QR code is malformed

1
changelog.d/6466.bugfix Normal file
View File

@ -0,0 +1 @@
When there is no way to verify a device (no 4S nor other device) propose to reset verification keys

1
changelog.d/6522.feature Normal file
View File

@ -0,0 +1 @@
Improve lock screen implementation with extra security measures

1
changelog.d/6548.feature Normal file
View File

@ -0,0 +1 @@
Move initialization of the Session to a background thread. MainActivity is restoring the session now, instead of VectorApplication. Useful when for instance a long migration of a database is required.

1
changelog.d/6607.misc Normal file
View File

@ -0,0 +1 @@
[Location sharing] - Small improvements of UI for live

1
changelog.d/6609.misc Normal file
View File

@ -0,0 +1 @@
Live Location Sharing - Reset zoom level while focusing a user

1
changelog.d/6612.misc Normal file
View File

@ -0,0 +1 @@
Fix a typo in the terms and conditions step during registration.

1
changelog.d/6616.feature Normal file
View File

@ -0,0 +1 @@
Support element call widget

1
changelog.d/6620.feature Normal file
View File

@ -0,0 +1 @@
FTUE - Test session feedback

1
changelog.d/6621.feature Normal file
View File

@ -0,0 +1 @@
FTUE - Improved reset password error message

1
changelog.d/6622.feature Normal file
View File

@ -0,0 +1 @@
FTUE - Allows the email address to be changed during the verification process

1
changelog.d/6625.misc Normal file
View File

@ -0,0 +1 @@
[Location sharing] - OnTap on the top live status bar, display the expanded map view

1
changelog.d/6634.bugfix Normal file
View File

@ -0,0 +1 @@
Put EC permission shortcuts behind labs flag (PSG-630)

1
changelog.d/6635.misc Normal file
View File

@ -0,0 +1 @@
[Location Share] - Expanded map state when no more live location shares

View File

@ -15,6 +15,7 @@ def gradle = "7.1.3"
def kotlin = "1.6.21" def kotlin = "1.6.21"
def kotlinCoroutines = "1.6.4" def kotlinCoroutines = "1.6.4"
def dagger = "2.42" def dagger = "2.42"
def appDistribution = "16.0.0-beta03"
def retrofit = "2.9.0" def retrofit = "2.9.0"
def arrow = "0.8.2" def arrow = "0.8.2"
def markwon = "4.6.2" def markwon = "4.6.2"
@ -49,9 +50,7 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
], ],
androidx : [ androidx : [
'annotation' : "androidx.annotation:annotation:1.4.0",
'activity' : "androidx.activity:activity:1.5.0", 'activity' : "androidx.activity:activity:1.5.0",
'annotations' : "androidx.annotation:annotation:1.3.0",
'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'biometric' : "androidx.biometric:biometric:1.1.0", 'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.8.0", 'core' : "androidx.core:core-ktx:1.8.0",
@ -83,7 +82,9 @@ ext.libs = [
'transition' : "androidx.transition:transition:1.2.0", 'transition' : "androidx.transition:transition:1.2.0",
], ],
google : [ google : [
'material' : "com.google.android.material:material:1.6.1" 'material' : "com.google.android.material:material:1.6.1",
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
], ],
dagger : [ dagger : [
'dagger' : "com.google.dagger:dagger:$dagger", 'dagger' : "com.google.dagger:dagger:$dagger",

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82 distributionSha256Sum=97a52d145762adc241bad7fd18289bf7f6801e08ece6badf80402fe2b9f250b1
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

6
gradlew vendored
View File

@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LocationLiveEndedBannerView">
<attr name="locLiveEndedBkgWithAlpha" format="boolean" />
<attr name="locLiveEndedIconMarginStart" format="dimension" />
</declare-styleable>
</resources>

View File

@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -47,7 +46,6 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class WithHeldTests : InstrumentedTest { class WithHeldTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @get:Rule val rule = RetryTestRule(3)

View File

@ -610,4 +610,82 @@ class SpaceHierarchyTest : InstrumentedTest {
} }
} }
} }
@Test
fun testDirectParentNames() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
commonTestHelper,
aliceSession, "SpaceA",
listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
val spaceBInfo = createPublicSpace(
commonTestHelper,
aliceSession, "SpaceB",
listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
// also add B1 in space A
val B1roomId = spaceBInfo.roomIds.first()
val viaServers = listOf(aliceSession.sessionParams.homeServerHost ?: "")
val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId)
val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId)
commonTestHelper.runBlockingTest {
spaceA!!.addChildren(B1roomId, viaServers, null, true)
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = aliceSession.getRoomSummary(B1roomId)
roomSummary != null &&
roomSummary.directParentNames.size == 2 &&
roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) &&
roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
}
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
roomSummary != null &&
roomSummary.directParentNames.size == 1 &&
roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name)
}
}
val newAName = "FooBar"
commonTestHelper.runBlockingTest {
spaceA!!.asRoom().stateService().updateName(newAName)
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = aliceSession.getRoomSummary(B1roomId)
roomSummary != null &&
roomSummary.directParentNames.size == 2 &&
roomSummary.directParentNames.contains(newAName) &&
roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
}
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
roomSummary != null &&
roomSummary.directParentNames.size == 1 &&
roomSummary.directParentNames.contains(newAName)
}
}
}
} }

View File

@ -86,6 +86,10 @@ fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection && fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection &&
this.ioException is UnknownHostException this.ioException is UnknownHostException
fun Throwable.isMissingEmailVerification() = this is Failure.ServerError &&
error.code == MatrixError.M_UNAUTHORIZED &&
error.message == "Unable to get validated threepid"
/** /**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/ */

View File

@ -180,11 +180,11 @@ class SecretStoringUtils @Inject constructor(
is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
else -> throw IllegalStateException("Unknown KeyEntry type.") else -> throw IllegalStateException("Unknown KeyEntry type.")
} }
val cipherMode = when { val cipherAlgorithm = when {
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
else -> RSA_MODE else -> RSA_MODE
} }
val cipher = Cipher.getInstance(cipherMode) val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, key) cipher.init(Cipher.ENCRYPT_MODE, key)
return cipher return cipher
} }
@ -204,13 +204,17 @@ class SecretStoringUtils @Inject constructor(
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(128) .setKeySize(128)
.setUserAuthenticationRequired(keyNeedsUserAuthentication)
.apply { .apply {
setUserAuthenticationRequired(keyNeedsUserAuthentication) if (keyNeedsUserAuthentication) {
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) { buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.N) {
setInvalidatedByBiometricEnrollment(true) setInvalidatedByBiometricEnrollment(true)
}
buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.P) {
setUnlockedDeviceRequired(true)
}
} }
} }
.setUserAuthenticationRequired(keyNeedsUserAuthentication)
.build() .build()
generator.init(keyGenSpec) generator.init(keyGenSpec)
return generator.generateKey() return generator.generateKey()

View File

@ -87,7 +87,10 @@ object EventType {
// Key share events // Key share events
const val ROOM_KEY_REQUEST = "m.room_key_request" const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld" val ROOM_KEY_WITHHELD = StableUnstableId(
stable = "m.room_key.withheld",
unstable = "org.matrix.room_key.withheld"
)
const val REQUEST_SECRET = "m.secret.request" const val REQUEST_SECRET = "m.secret.request"
const val SEND_SECRET = "m.secret.send" const val SEND_SECRET = "m.secret.send"

View File

@ -0,0 +1,24 @@
/*
* 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.session.events.model
data class StableUnstableId(
val stable: String,
val unstable: String,
) {
val values = listOf(stable, unstable)
}

View File

@ -243,14 +243,11 @@ interface RoomService {
* @param queryParams The filter to use * @param queryParams The filter to use
* @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...) * @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
* @param sortOrder defines how to sort the results * @param sortOrder defines how to sort the results
* @param getFlattenParents When true, the list of known parents and grand parents summaries will be resolved.
* This can have significant impact on performance, better be used only on manageable list (filtered by displayName, ..).
*/ */
fun getFilteredPagedRoomSummariesLive( fun getFilteredPagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig, pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY, sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
getFlattenParents: Boolean = false,
): UpdatableLivePageResult ): UpdatableLivePageResult
/** /**

View File

@ -164,9 +164,9 @@ data class RoomSummary(
*/ */
val spaceChildren: List<SpaceChildInfo>? = null, val spaceChildren: List<SpaceChildInfo>? = null,
/** /**
* List of all the space parents. Will be empty by default, you have to explicitly request it. * The names of the room's direct space parents if any.
*/ */
val flattenParents: List<RoomSummary> = emptyList(), val directParentNames: List<String> = emptyList(),
/** /**
* List of all the space parent Ids. * List of all the space parent Ids.
*/ */

View File

@ -60,9 +60,9 @@ interface SyncService {
fun getSyncStateLive(): LiveData<SyncState> fun getSyncStateLive(): LiveData<SyncState>
/** /**
* Get the [SyncRequestState] as a LiveData. * Get the [SyncRequestState] as a SharedFlow.
*/ */
fun getSyncRequestStateLive(): LiveData<SyncRequestState> fun getSyncRequestStateFlow(): SharedFlow<SyncRequestState>
/** /**
* This method returns a flow of SyncResponse. New value will be pushed through the sync thread. * This method returns a flow of SyncResponse. New value will be pushed through the sync thread.

View File

@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy {
WidgetType.StickerPicker, WidgetType.StickerPicker,
WidgetType.Grafana, WidgetType.Grafana,
WidgetType.Custom, WidgetType.Custom,
WidgetType.IntegrationManager WidgetType.IntegrationManager,
WidgetType.ElementCall,
) )
} }
@ -47,6 +48,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
object Grafana : WidgetType("m.grafana") object Grafana : WidgetType("m.grafana")
object Custom : WidgetType("m.custom") object Custom : WidgetType("m.custom")
object IntegrationManager : WidgetType("m.integration_manager") object IntegrationManager : WidgetType("m.integration_manager")
object ElementCall : WidgetType("io.element.call")
data class Fallback(override val preferred: String) : WidgetType(preferred) data class Fallback(override val preferred: String) : WidgetType(preferred)
fun matches(type: String): Boolean { fun matches(type: String): Boolean {

View File

@ -21,4 +21,14 @@ interface BuildVersionSdkIntProvider {
* Return the current version of the Android SDK. * Return the current version of the Android SDK.
*/ */
fun get(): Int fun get(): Int
/**
* Checks the if the current OS version is equal or greater than [version].
* @return A `non-null` result if true, `null` otherwise.
*/
fun <T> whenAtLeast(version: Int, result: () -> T): T? {
return if (get() >= version) {
result()
} else null
}
} }

View File

@ -820,7 +820,7 @@ internal class DefaultCryptoService @Inject constructor(
EventType.SEND_SECRET -> { EventType.SEND_SECRET -> {
onSecretSendReceived(event) onSecretSendReceived(event)
} }
EventType.ROOM_KEY_WITHHELD -> { in EventType.ROOM_KEY_WITHHELD.values -> {
onKeyWithHeldReceived(event) onKeyWithHeldReceived(event)
} }
else -> { else -> {
@ -869,7 +869,7 @@ internal class DefaultCryptoService @Inject constructor(
senderKey = withHeldContent.senderKey, senderKey = withHeldContent.senderKey,
fromDevice = withHeldContent.fromDevice, fromDevice = withHeldContent.fromDevice,
event = Event( event = Event(
type = EventType.ROOM_KEY_WITHHELD, type = EventType.ROOM_KEY_WITHHELD.stable,
senderId = senderId, senderId = senderId,
content = event.getClearContent() content = event.getClearContent()
) )

View File

@ -315,7 +315,7 @@ internal class IncomingKeyRequestManager @Inject constructor(
) )
val params = SendToDeviceTask.Params( val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD, EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap<Any>().apply { MXUsersDevicesMap<Any>().apply {
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent) setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
} }

View File

@ -365,7 +365,7 @@ internal class MXMegolmEncryption(
fromDevice = myDeviceId fromDevice = myDeviceId
) )
val params = SendToDeviceTask.Params( val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD, EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap<Any>().apply { MXUsersDevicesMap<Any>().apply {
targets.forEach { targets.forEach {
setObject(it.userId, it.deviceId, withHeldContent) setObject(it.userId, it.deviceId, withHeldContent)

View File

@ -117,7 +117,7 @@ internal open class OutgoingKeyRequestEntity(
private fun eventToResult(event: Event): RequestResult? { private fun eventToResult(event: Event): RequestResult? {
return when (event.getClearType()) { return when (event.getClearType()) {
EventType.ROOM_KEY_WITHHELD -> { in EventType.ROOM_KEY_WITHHELD.values -> {
event.content.toModel<RoomKeyWithHeldContent>()?.code?.let { event.content.toModel<RoomKeyWithHeldContent>()?.code?.let {
RequestResult.Failure(it) RequestResult.Failure(it)
} }

View File

@ -84,7 +84,7 @@ internal class DefaultQrCodeVerificationTransaction(
// Perform some checks // Perform some checks
if (otherQrCodeData.transactionId != transactionId) { if (otherQrCodeData.transactionId != transactionId) {
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId") Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
cancel(CancelCode.QrCodeInvalid) cancel(CancelCode.UnknownTransaction)
return return
} }

View File

@ -51,6 +51,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -59,7 +60,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 34L, schemaVersion = 35L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -103,5 +104,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 32) MigrateSessionTo032(realm).perform() if (oldVersion < 32) MigrateSessionTo032(realm).perform()
if (oldVersion < 33) MigrateSessionTo033(realm).perform() if (oldVersion < 33) MigrateSessionTo033(realm).perform()
if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 34) MigrateSessionTo034(realm).perform()
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
} }
} }

View File

@ -106,6 +106,7 @@ internal class RoomSummaryMapper @Inject constructor(
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
) )
}, },
directParentNames = roomSummaryEntity.directParentNames.toList(),
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(), flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) { roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
// I should probably use #hasEncryptorClassForAlgorithm but it says it supports // I should probably use #hasEncryptorClassForAlgorithm but it says it supports

View File

@ -0,0 +1,31 @@
/*
* 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.database.migration
import io.realm.DynamicRealm
import io.realm.RealmList
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo035(realm: DynamicRealm) : RealmMigrator(realm, 35) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("RoomSummaryEntity")
?.addRealmListField(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, String::class.java)
?.transform { it.setList(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, RealmList("")) }
}
}

View File

@ -34,7 +34,8 @@ internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "", @PrimaryKey var roomId: String = "",
var roomType: String? = null, var roomType: String? = null,
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(), var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
var children: RealmList<SpaceChildSummaryEntity> = RealmList() var children: RealmList<SpaceChildSummaryEntity> = RealmList(),
var directParentNames: RealmList<String> = RealmList(),
) : RealmObject() { ) : RealmObject() {
private var displayName: String? = "" private var displayName: String? = ""

View File

@ -152,9 +152,8 @@ internal class DefaultRoomService @Inject constructor(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config, pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder, sortOrder: RoomSortOrder,
getFlattenParents: Boolean
): UpdatableLivePageResult { ): UpdatableLivePageResult {
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents) return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
} }
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> { override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {

View File

@ -701,7 +701,7 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
MessageType.MSGTYPE_BEACON_INFO -> return TextContent(content.body.ensureNotEmpty() ?: "shared live location.") MessageType.MSGTYPE_BEACON_INFO -> return TextContent(content.body.ensureNotEmpty() ?: "Live location")
MessageType.MSGTYPE_POLL_START -> { MessageType.MSGTYPE_POLL_START -> {
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
} }

View File

@ -200,14 +200,13 @@ internal class RoomSummaryDataSource @Inject constructor(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config, pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder, sortOrder: RoomSortOrder,
getFlattenedParents: Boolean = false
): UpdatableLivePageResult { ): UpdatableLivePageResult {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams).process(sortOrder) roomSummariesQuery(realm, queryParams).process(sortOrder)
} }
val dataSourceFactory = realmDataSourceFactory.map { val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it) roomSummaryMapper.map(it)
}.map { if (getFlattenedParents) it.getWithParents() else it } }
val boundaries = MutableLiveData(ResultBoundaries()) val boundaries = MutableLiveData(ResultBoundaries())
@ -246,13 +245,6 @@ internal class RoomSummaryDataSource @Inject constructor(
} }
} }
private fun RoomSummary.getWithParents(): RoomSummary {
val parents = flattenParentIds.mapNotNull { parentId ->
getRoomSummary(parentId)
}
return copy(flattenParents = parents)
}
fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> { fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
val liveRooms = monarchy.findAllManagedWithChanges { val liveRooms = monarchy.findAllManagedWithChanges {
roomSummariesQuery(it, queryParams) roomSummariesQuery(it, queryParams)

View File

@ -223,6 +223,7 @@ internal class RoomSummaryUpdater @Inject constructor(
.sort(RoomSummaryEntityFields.ROOM_ID) .sort(RoomSummaryEntityFields.ROOM_ID)
.findAll().map { .findAll().map {
it.flattenParentIds = null it.flattenParentIds = null
it.directParentNames.clear()
it to emptyList<RoomSummaryEntity>().toMutableSet() it to emptyList<RoomSummaryEntity>().toMutableSet()
} }
.toMap() .toMap()
@ -350,39 +351,29 @@ internal class RoomSummaryUpdater @Inject constructor(
} }
val acyclicGraph = graph.withoutEdges(backEdges) val acyclicGraph = graph.withoutEdges(backEdges)
// Timber.v("## SPACES: acyclicGraph $acyclicGraph")
val flattenSpaceParents = acyclicGraph.flattenDestination().map { val flattenSpaceParents = acyclicGraph.flattenDestination().map {
it.key.name to it.value.map { it.name } it.key.name to it.value.map { it.name }
}.toMap() }.toMap()
// Timber.v("## SPACES: flattenSpaceParents ${flattenSpaceParents.map { it.key.name to it.value.map { it.name } }.joinToString("\n") {
// it.first + ": [" + it.second.joinToString(",") + "]"
// }}")
// Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}")
lookupMap.entries lookupMap.entries
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN } .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
.forEach { entry -> .forEach { entry ->
val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst() val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
if (parent != null) { if (parent != null) {
// Timber.v("## SPACES: check hierarchy of ${parent.name} id ${parent.roomId}")
// Timber.v("## SPACES: flat known parents of ${parent.name} are ${flattenSpaceParents[parent.roomId]}")
val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId) val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId)
// Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}")
entry.value.forEach { child -> entry.value.forEach { child ->
RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum -> RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
childSum.directParentNames.add(parent.displayName())
// Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}") if (childSum.flattenParentIds == null) {
// var allParents = childSum.flattenParentIds ?: "" childSum.flattenParentIds = ""
if (childSum.flattenParentIds == null) childSum.flattenParentIds = "" }
flattenParentsIds.forEach { flattenParentsIds.forEach {
if (childSum.flattenParentIds?.contains(it) != true) { if (childSum.flattenParentIds?.contains(it) != true) {
childSum.flattenParentIds += "|$it" childSum.flattenParentIds += "|$it"
} }
} }
// childSum.flattenParentIds = "$allParents|"
// Timber.v("## SPACES: flatten of ${childSum.name} is ${childSum.flattenParentIds}")
} }
} }
} }

View File

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.sync.SyncRequestState
import org.matrix.android.sdk.api.session.sync.SyncService import org.matrix.android.sdk.api.session.sync.SyncService
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
@ -75,9 +73,7 @@ internal class DefaultSyncService @Inject constructor(
override fun getSyncState() = getSyncThread().currentState() override fun getSyncState() = getSyncThread().currentState()
override fun getSyncRequestStateLive(): LiveData<SyncRequestState> { override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
return syncRequestStateTracker.syncRequestState
}
override fun hasAlreadySynced(): Boolean { override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null return syncTokenStore.getLastToken() != null

View File

@ -16,23 +16,26 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.sync.InitialSyncStep import org.matrix.android.sdk.api.session.sync.InitialSyncStep
import org.matrix.android.sdk.api.session.sync.SyncRequestState import org.matrix.android.sdk.api.session.sync.SyncRequestState
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class SyncRequestStateTracker @Inject constructor() : internal class SyncRequestStateTracker @Inject constructor(
ProgressReporter { private val coroutineScope: CoroutineScope
) : ProgressReporter {
val syncRequestState = MutableLiveData<SyncRequestState>() val syncRequestState = MutableSharedFlow<SyncRequestState>()
private var rootTask: TaskInfo? = null private var rootTask: TaskInfo? = null
// Only to be used for incremental sync // Only to be used for incremental sync
fun setSyncRequestState(newSyncRequestState: SyncRequestState.IncrementalSyncRequestState) { fun setSyncRequestState(newSyncRequestState: SyncRequestState.IncrementalSyncRequestState) {
syncRequestState.postValue(newSyncRequestState) emitSyncState(newSyncRequestState)
} }
/** /**
@ -42,7 +45,9 @@ internal class SyncRequestStateTracker @Inject constructor() :
initialSyncStep: InitialSyncStep, initialSyncStep: InitialSyncStep,
totalProgress: Int totalProgress: Int
) { ) {
endAll() if (rootTask != null) {
endAll()
}
rootTask = TaskInfo(initialSyncStep, totalProgress, null, 1F) rootTask = TaskInfo(initialSyncStep, totalProgress, null, 1F)
reportProgress(0F) reportProgress(0F)
} }
@ -71,7 +76,7 @@ internal class SyncRequestStateTracker @Inject constructor() :
// Update the progress of the leaf and all its parents // Update the progress of the leaf and all its parents
leaf.setProgress(progress) leaf.setProgress(progress)
// Then update the live data using leaf wording and root progress // Then update the live data using leaf wording and root progress
syncRequestState.postValue(SyncRequestState.InitialSyncProgressing(leaf.initialSyncStep, root.currentProgress.toInt())) emitSyncState(SyncRequestState.InitialSyncProgressing(leaf.initialSyncStep, root.currentProgress.toInt()))
} }
} }
} }
@ -86,13 +91,19 @@ internal class SyncRequestStateTracker @Inject constructor() :
// And close it // And close it
endedTask.parent.child = null endedTask.parent.child = null
} else { } else {
syncRequestState.postValue(SyncRequestState.Idle) emitSyncState(SyncRequestState.Idle)
} }
} }
} }
fun endAll() { fun endAll() {
rootTask = null rootTask = null
syncRequestState.postValue(SyncRequestState.Idle) emitSyncState(SyncRequestState.Idle)
}
private fun emitSyncState(state: SyncRequestState) {
coroutineScope.launch {
syncRequestState.emit(state)
}
} }
} }

View File

@ -449,6 +449,12 @@ dependencies {
implementation libs.airbnb.epoxyPaging implementation libs.airbnb.epoxyPaging
implementation libs.airbnb.mavericks implementation libs.airbnb.mavericks
// Nightly
// API-only library
gplayImplementation libs.google.appdistributionApi
// Full SDK implementation
gplayImplementation libs.google.appdistribution
// Work // Work
implementation libs.androidx.work implementation libs.androidx.work

View File

@ -0,0 +1,75 @@
/*
* 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.view.View
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import im.vector.app.features.MainActivity
import im.vector.app.ui.robot.ElementRobot
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@LargeTest
class CantVerifyTest : VerificationTestBase() {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val elementRobot = ElementRobot()
var userName: String = "loginTest_${UUID.randomUUID()}"
@Test
fun checkCantVerifyPopup() {
// Let' create an account
// This first session will create cross signing keys then logout
elementRobot.signUp(userName)
Espresso.onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000))
elementRobot.signout(false)
Espresso.onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000))
// Let's login again now
// There are no methods to verify (no other devices, nor 4S)
// So it should ask to reset all
elementRobot.login(userName)
val activity = EspressoHelper.getCurrentActivity()!!
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withText(R.string.crosssigning_cannot_verify_this_session)))
// check that the text is correct
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)!!
activity.runOnUiThread { popup.performClick() }
// ensure that it's the 4S setup bottomsheet
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetFragmentContainer))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000))
Espresso.onView(ViewMatchers.withText(R.string.bottom_sheet_setup_secure_backup_title))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
}

View File

@ -24,13 +24,16 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.TestBuildVersionSdkIntProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
@ -56,6 +59,9 @@ import org.amshove.kluent.shouldBeTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import java.security.KeyStore
import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -64,6 +70,13 @@ class BiometricHelperTests {
private val biometricManager = mockk<BiometricManager>(relaxed = true) private val biometricManager = mockk<BiometricManager>(relaxed = true)
private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true) private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true)
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
private val keyStore = KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
private val secretStoringUtils = SecretStoringUtils(
InstrumentationRegistry.getInstrumentation().targetContext,
keyStore,
buildVersionSdkIntProvider,
false,
)
@Before @Before
fun setup() { fun setup() {
@ -188,8 +201,10 @@ class BiometricHelperTests {
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
@Test @Test
fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest { fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
mockkStatic("kotlinx.coroutines.flow.FlowKt") mockkStatic("kotlinx.coroutines.flow.FlowKt")
val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) { val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) {
// Empty flow to keep the dialog open // Empty flow to keep the dialog open
@ -201,6 +216,9 @@ class BiometricHelperTests {
mockkObject(DevicePromptCheck) mockkObject(DevicePromptCheck)
every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true
every { lockScreenKeyRepository.isSystemKeyValid() } returns true every { lockScreenKeyRepository.isSystemKeyValid() } returns true
val keyAlias = UUID.randomUUID().toString()
every { biometricUtils.getAuthCryptoObject() } returns BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(keyAlias))
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java) val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
with(ActivityScenario.launch<LockScreenTestActivity>(intent)) { with(ActivityScenario.launch<LockScreenTestActivity>(intent)) {
@ -214,11 +232,13 @@ class BiometricHelperTests {
} }
} }
latch.await(1, TimeUnit.SECONDS) latch.await(1, TimeUnit.SECONDS)
keyStore.deleteEntry(keyAlias)
unmockkObject(DevicePromptCheck) unmockkObject(DevicePromptCheck)
unmockkStatic("kotlinx.coroutines.flow.FlowKt") unmockkStatic("kotlinx.coroutines.flow.FlowKt")
} }
@Test @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest { fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
every { lockScreenKeyRepository.isSystemKeyValid() } returns true every { lockScreenKeyRepository.isSystemKeyValid() } returns true

View File

@ -43,7 +43,9 @@ class KeyStoreCryptoTests {
private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M } private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider)) private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider))
private val keyStoreCrypto = spyk( private val keyStoreCrypto = spyk(
KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils) KeyStoreCrypto(alias, false, context, versionProvider, keyStore).also {
it.secretStoringUtils = secretStoringUtils
}
) )
@After @After
@ -146,10 +148,10 @@ class KeyStoreCryptoTests {
@Test @Test
fun getCryptoObjectUsesCipherFromSecretStoringUtils() { fun getCryptoObjectUsesCipherFromSecretStoringUtils() {
keyStoreCrypto.getCryptoObject() keyStoreCrypto.getAuthCryptoObject()
verify { secretStoringUtils.getEncryptCipher(any()) } verify { secretStoringUtils.getEncryptCipher(any()) }
every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException() every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
invoking { keyStoreCrypto.getCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class invoking { keyStoreCrypto.getAuthCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class
} }
} }

View File

@ -16,21 +16,16 @@
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto
import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldNotThrow
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -49,7 +44,7 @@ class LockScreenKeyRepositoryTests {
} }
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
private val pinCodeMigrator: PinCodeMigrator = mockk(relaxed = true) private val legacyPinCodeMigrator: LegacyPinCodeMigrator = mockk(relaxed = true)
private val vectorPreferences: VectorPreferences = mockk(relaxed = true) private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
private val keyStore: KeyStore by lazy { private val keyStore: KeyStore by lazy {
@ -58,7 +53,7 @@ class LockScreenKeyRepositoryTests {
@Before @Before
fun setup() { fun setup() {
lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory)) lockScreenKeyRepository = spyk(LockScreenKeyRepository("base.pin_code", "base.system", keyStoreCryptoFactory))
} }
@After @After
@ -141,44 +136,4 @@ class LockScreenKeyRepositoryTests {
lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse() lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
} }
@Test
fun migrateKeysIfNeededReturnsEarlyIfNotNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false
lockScreenKeyRepository.migrateKeysIfNeeded()
coVerify(exactly = 0) { pinCodeMigrator.migrate(any()) }
}
@Test
fun migrateKeysIfNeededWillMigratePinCodeAndKeys() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
lockScreenKeyRepository.migrateKeysIfNeeded()
coVerify { pinCodeMigrator.migrate(any()) }
}
@Test
fun migrateKeysIfNeededWillCreateSystemKeyIfNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
every { vectorPreferences.useBiometricsToUnlock() } returns true
every { lockScreenKeyRepository.ensureSystemKey() } returns mockk()
lockScreenKeyRepository.migrateKeysIfNeeded()
verify { lockScreenKeyRepository.ensureSystemKey() }
}
@Test
fun migrateKeysIfNeededWillHandleKeyPermanentlyInvalidatedException() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
every { vectorPreferences.useBiometricsToUnlock() } returns true
every { lockScreenKeyRepository.ensureSystemKey() } throws KeyPermanentlyInvalidatedException()
coInvoking { lockScreenKeyRepository.migrateKeysIfNeeded() } shouldNotThrow KeyPermanentlyInvalidatedException::class
verify { lockScreenKeyRepository.ensureSystemKey() }
}
} }

View File

@ -16,7 +16,7 @@
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto.migrations
import android.os.Build import android.os.Build
import android.security.KeyPairGeneratorSpec import android.security.KeyPairGeneratorSpec
@ -57,7 +57,7 @@ import javax.crypto.spec.PSource
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import kotlin.math.abs import kotlin.math.abs
class PinCodeMigratorTests { class LegacyPinCodeMigratorTests {
private val alias = UUID.randomUUID().toString() private val alias = UUID.randomUUID().toString()
@ -72,7 +72,9 @@ class PinCodeMigratorTests {
private val secretStoringUtils: SecretStoringUtils = spyk( private val secretStoringUtils: SecretStoringUtils = spyk(
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
) )
private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)) private val legacyPinCodeMigrator = spyk(
LegacyPinCodeMigrator(alias, pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)
)
@After @After
fun tearDown() { fun tearDown() {
@ -87,21 +89,21 @@ class PinCodeMigratorTests {
@Test @Test
fun isMigrationNeededReturnsTrueIfLegacyKeyExists() { fun isMigrationNeededReturnsTrueIfLegacyKeyExists() {
pinCodeMigrator.isMigrationNeeded() shouldBe false legacyPinCodeMigrator.isMigrationNeeded() shouldBe false
generateLegacyKey() generateLegacyKey()
pinCodeMigrator.isMigrationNeeded() shouldBe true legacyPinCodeMigrator.isMigrationNeeded() shouldBe true
} }
@Test @Test
fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest { fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
coEvery { pinCodeStore.getPinCode() } returns null coEvery { pinCodeStore.getPinCode() } returns null
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } coVerify(exactly = 0) { legacyPinCodeMigrator.getDecryptedPinCode() }
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -109,13 +111,13 @@ class PinCodeMigratorTests {
@Test @Test
fun migrateWillReturnEarlyIfIsNotNeeded() = runTest { fun migrateWillReturnEarlyIfIsNotNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234" coEvery { legacyPinCodeMigrator.getDecryptedPinCode() } returns "1234"
every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0) every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } coVerify(exactly = 0) { legacyPinCodeMigrator.getDecryptedPinCode() }
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -126,9 +128,9 @@ class PinCodeMigratorTests {
val pinCode = "1234" val pinCode = "1234"
saveLegacyPinCode(pinCode) saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() } coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) } verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) } coVerify { pinCodeStore.savePinCode(any()) }
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -145,9 +147,9 @@ class PinCodeMigratorTests {
every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
saveLegacyPinCode(pinCode) saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() } coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) } verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) } coVerify { pinCodeStore.savePinCode(any()) }
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }

View File

@ -23,7 +23,6 @@ import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
@ -182,13 +181,8 @@ class ElementRobot {
val activity = EspressoHelper.getCurrentActivity()!! val activity = EspressoHelper.getCurrentActivity()!!
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)!! val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)!!
activity.runOnUiThread { popup.performClick() } activity.runOnUiThread { popup.performClick() }
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
waitUntilViewVisible(ViewMatchers.withText(R.string.action_skip))
clickOn(R.string.action_skip)
assertDisplayed(R.string.are_you_sure)
clickOn(R.string.action_skip)
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer)) waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
pressBack()
}.onFailure { Timber.w(it, "Verification popup missing") } }.onFailure { Timber.w(it, "Verification popup missing") }
} }

View File

@ -34,31 +34,46 @@ import im.vector.app.waitForView
class OnboardingRobot { class OnboardingRobot {
private val defaultVectorFeatures = DefaultVectorFeatures()
fun crawl() { fun crawl() {
waitUntilViewVisible(withId(R.id.loginSplashSubmit)) waitUntilViewVisible(withId(R.id.loginSplashSubmit))
crawlGetStarted() crawlCreateAccount()
crawlAlreadyHaveAccount() crawlAlreadyHaveAccount()
} }
private fun crawlGetStarted() { private fun crawlCreateAccount() {
clickOn(R.id.loginSplashSubmit) if (defaultVectorFeatures.isOnboardingCombinedRegisterEnabled()) {
assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title) // TODO https://github.com/vector-im/element-android/issues/6652
clickOn(R.id.useCaseOptionOne) } else {
OnboardingServersRobot().crawlSignUp() clickOn(R.id.loginSplashSubmit)
pressBack() assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title)
pressBack() clickOn(R.id.useCaseOptionOne)
OnboardingServersRobot().crawlSignUp()
pressBack()
pressBack()
}
} }
private fun crawlAlreadyHaveAccount() { private fun crawlAlreadyHaveAccount() {
clickOn(R.id.loginSplashAlreadyHaveAccount) if (defaultVectorFeatures.isOnboardingCombinedLoginEnabled()) {
OnboardingServersRobot().crawlSignIn() // TODO https://github.com/vector-im/element-android/issues/6652
pressBack() } else {
clickOn(R.id.loginSplashAlreadyHaveAccount)
OnboardingServersRobot().crawlSignIn()
pressBack()
}
} }
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl) if (defaultVectorFeatures.isOnboardingCombinedRegisterEnabled()) {
createAccountViaCombinedRegister(homeServerUrl, userId, password)
} else {
initSession(true, userId, password, homeServerUrl)
}
waitUntilViewVisible(withText(R.string.ftue_account_created_congratulations_title)) waitUntilViewVisible(withText(R.string.ftue_account_created_congratulations_title))
if (DefaultVectorFeatures().isOnboardingPersonalizeEnabled()) { if (defaultVectorFeatures.isOnboardingPersonalizeEnabled()) {
clickOn(R.string.ftue_account_created_personalize) clickOn(R.string.ftue_account_created_personalize)
waitUntilViewVisible(withText(R.string.ftue_display_name_title)) waitUntilViewVisible(withText(R.string.ftue_display_name_title))
@ -75,8 +90,47 @@ class OnboardingRobot {
} }
} }
private fun createAccountViaCombinedRegister(homeServerUrl: String, userId: String, password: String) {
waitUntilViewVisible(withId(R.id.loginSplashSubmit))
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
clickOn(R.id.loginSplashSubmit)
clickOn(R.id.useCaseOptionOne)
waitUntilViewVisible(withId(R.id.createAccountRoot))
clickOn(R.id.editServerButton)
writeTo(R.id.chooseServerInput, homeServerUrl)
closeSoftKeyboard()
clickOn(R.id.chooseServerSubmit)
waitUntilViewVisible(withId(R.id.createAccountRoot))
writeTo(R.id.createAccountInput, userId)
writeTo(R.id.createAccountPasswordInput, password)
clickOn(R.id.createAccountSubmit)
}
fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(false, userId, password, homeServerUrl) if (defaultVectorFeatures.isOnboardingCombinedLoginEnabled()) {
loginViaCombinedLogin(homeServerUrl, userId, password)
} else {
initSession(false, userId, password, homeServerUrl)
}
}
private fun loginViaCombinedLogin(homeServerUrl: String, userId: String, password: String) {
waitUntilViewVisible(withId(R.id.loginSplashSubmit))
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
clickOn(R.id.loginSplashAlreadyHaveAccount)
waitUntilViewVisible(withId(R.id.loginRoot))
clickOn(R.id.editServerButton)
writeTo(R.id.chooseServerInput, homeServerUrl)
closeSoftKeyboard()
clickOn(R.id.chooseServerSubmit)
waitUntilViewVisible(withId(R.id.loginRoot))
writeTo(R.id.loginInput, userId)
writeTo(R.id.loginPasswordInput, password)
clickOn(R.id.loginSubmit)
} }
private fun initSession( private fun initSession(

View File

@ -17,13 +17,12 @@
package im.vector.app.ui.robot.settings package im.vector.app.ui.robot.settings
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.settings.font.FontScaleSettingActivity
class SettingsPreferencesRobot { class SettingsPreferencesRobot {
@ -34,8 +33,7 @@ class SettingsPreferencesRobot {
clickOn(R.string.settings_theme) clickOn(R.string.settings_theme)
clickDialogNegativeButton() clickDialogNegativeButton()
clickOn(R.string.font_size) clickOn(R.string.font_size)
waitUntilActivityVisible<FontScaleSettingActivity> { waitUntilViewVisible(withId(R.id.fons_scale_recycler))
pressBack() pressBack()
}
} }
} }

View File

@ -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.nightly
import javax.inject.Inject
class NightlyProxy @Inject constructor() {
fun onHomeResumed() = Unit
}

View File

@ -0,0 +1,76 @@
/*
* 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.nightly
import android.content.SharedPreferences
import androidx.core.content.edit
import com.google.firebase.appdistribution.FirebaseAppDistribution
import com.google.firebase.appdistribution.FirebaseAppDistributionException
import im.vector.app.BuildConfig
import im.vector.app.core.di.DefaultPreferences
import im.vector.app.core.time.Clock
import timber.log.Timber
import javax.inject.Inject
class NightlyProxy @Inject constructor(
private val clock: Clock,
@DefaultPreferences
private val sharedPreferences: SharedPreferences,
) {
fun onHomeResumed() {
if (!canDisplayPopup()) return
val firebaseAppDistribution = FirebaseAppDistribution.getInstance()
firebaseAppDistribution.updateIfNewReleaseAvailable()
.addOnProgressListener { up ->
Timber.d("FirebaseAppDistribution progress: ${up.updateStatus}. ${up.apkBytesDownloaded}/${up.apkFileTotalBytes}")
}
.addOnFailureListener { e ->
if (e is FirebaseAppDistributionException) {
when (e.errorCode) {
FirebaseAppDistributionException.Status.NOT_IMPLEMENTED -> {
// SDK did nothing. This is expected when building for Play.
}
else -> {
// Handle other errors.
Timber.e(e, "FirebaseAppDistribution error, status: ${e.errorCode}")
}
}
} else {
Timber.e(e, "FirebaseAppDistribution - other error")
}
}
}
private fun canDisplayPopup(): Boolean {
if (BuildConfig.APPLICATION_ID != "im.vector.app.nightly") return false
val today = clock.epochMillis() / A_DAY_IN_MILLIS
val lastDisplayPopupDay = sharedPreferences.getLong(SHARED_PREF_KEY, 0)
return (today > lastDisplayPopupDay)
.also { canDisplayPopup ->
if (canDisplayPopup) {
sharedPreferences.edit {
putLong(SHARED_PREF_KEY, today)
}
}
}
}
companion object {
private const val A_DAY_IN_MILLIS = 8_600_000L
private const val SHARED_PREF_KEY = "LAST_NIGHTLY_POPUP_DAY"
}
}

View File

@ -308,7 +308,8 @@
<activity android:name=".features.terms.ReviewTermsActivity" /> <activity android:name=".features.terms.ReviewTermsActivity" />
<activity <activity
android:name=".features.widgets.WidgetActivity" android:name=".features.widgets.WidgetActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" /> android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:supportsPictureInPicture="true" />
<activity android:name=".features.pin.PinActivity" /> <activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" /> <activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" />
@ -380,6 +381,11 @@
android:exported="false" android:exported="false"
android:foregroundServiceType="location" /> android:foregroundServiceType="location" />
<service
android:name=".features.start.StartAppAndroidService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service <service
android:name=".features.call.webrtc.ScreenCaptureAndroidService" android:name=".features.call.webrtc.ScreenCaptureAndroidService"
android:exported="false" android:exported="false"

View File

@ -17,7 +17,6 @@
package im.vector.app package im.vector.app
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.rageshake.ReportType import im.vector.app.features.rageshake.ReportType
@ -261,8 +260,7 @@ class AutoRageShaker @Inject constructor(
this.currentActiveSessionId = sessionId this.currentActiveSessionId = sessionId
hasSynced = session.syncService().hasAlreadySynced() hasSynced = session.syncService().hasAlreadySynced()
session.syncService().getSyncRequestStateLive() session.syncService().getSyncRequestStateFlow()
.asFlow()
.onEach { .onEach {
hasSynced = it !is SyncRequestState.InitialSyncProgressing hasSynced = it !is SyncRequestState.InitialSyncProgressing
} }

View File

@ -17,7 +17,6 @@
package im.vector.app package im.vector.app
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.asFlow
import arrow.core.Option import arrow.core.Option
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.core.utils.BehaviorDataSource
@ -119,8 +118,7 @@ class SpaceStateHandlerImpl @Inject constructor(
} }
private fun observeSyncStatus(session: Session) { private fun observeSyncStatus(session: Session) {
session.syncService().getSyncRequestStateLive() session.syncService().getSyncRequestStateFlow()
.asFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncDone>() .filterIsInstance<SyncRequestState.IncrementalSyncDone>()
.map { session.spaceService().getRootSpaceSummaries().size } .map { session.spaceService().getRootSpaceSummaries().size }
.distinctUntilChanged() .distinctUntilChanged()

View File

@ -41,8 +41,6 @@ import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider import com.vanniktech.emoji.google.GoogleEmojiProvider
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import im.vector.app.core.di.ActiveSessionHolder 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.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
@ -165,14 +163,6 @@ class VectorApplication :
doNotShowDisclaimerDialog(this) doNotShowDisclaimerDialog(this)
} }
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(applicationContext, startSyncing = false)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(startSyncOnFirstStart)
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
Timber.i("App entered foreground") Timber.i("App entered foreground")
@ -205,14 +195,6 @@ class VectorApplication :
Mapbox.getInstance(this) Mapbox.getInstance(this)
} }
private val startSyncOnFirstStart = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.i("App process started")
authenticationService.getLastAuthenticatedSession()?.startSyncing(appContext)
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
}
}
private fun enableStrictModeIfNeeded() { private fun enableStrictModeIfNeeded() {
if (BuildConfig.ENABLE_STRICT_MODE_LOGS) { if (BuildConfig.ENABLE_STRICT_MODE_LOGS) {
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(

View File

@ -0,0 +1,40 @@
/*
* Copyright 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.di
import android.content.Context
import im.vector.app.core.extensions.configureAndStart
import org.matrix.android.sdk.api.auth.AuthenticationService
import javax.inject.Inject
class ActiveSessionSetter @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val authenticationService: AuthenticationService,
private val applicationContext: Context,
) {
fun shouldSetActionSession(): Boolean {
return authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()
}
fun tryToSetActiveSession(startSync: Boolean) {
if (shouldSetActionSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(applicationContext, startSyncing = startSync)
}
}
}

View File

@ -61,6 +61,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.TimelineFragment
import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import im.vector.app.features.location.LocationPreviewFragment import im.vector.app.features.location.LocationPreviewFragment
import im.vector.app.features.location.LocationSharingFragment import im.vector.app.features.location.LocationSharingFragment
@ -1041,4 +1042,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(LocationPreviewFragment::class) @FragmentKey(LocationPreviewFragment::class)
fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment
@Binds
@IntoMap
@FragmentKey(HomeRoomListFragment::class)
fun binHomeRoomListFragment(fragment: HomeRoomListFragment): Fragment
} }

View File

@ -51,6 +51,7 @@ import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHist
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsViewModel import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel
import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.location.LocationSharingViewModel
@ -111,6 +112,7 @@ import im.vector.app.features.spaces.manage.SpaceManageSharedViewModel
import im.vector.app.features.spaces.people.SpacePeopleViewModel import im.vector.app.features.spaces.people.SpacePeopleViewModel
import im.vector.app.features.spaces.preview.SpacePreviewViewModel import im.vector.app.features.spaces.preview.SpacePreviewViewModel
import im.vector.app.features.spaces.share.ShareSpaceViewModel import im.vector.app.features.spaces.share.ShareSpaceViewModel
import im.vector.app.features.start.StartAppViewModel
import im.vector.app.features.terms.ReviewTermsViewModel import im.vector.app.features.terms.ReviewTermsViewModel
import im.vector.app.features.usercode.UserCodeSharedViewModel import im.vector.app.features.usercode.UserCodeSharedViewModel
import im.vector.app.features.userdirectory.UserListViewModel import im.vector.app.features.userdirectory.UserListViewModel
@ -483,6 +485,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(AnalyticsAccountDataViewModel::class) @MavericksViewModelKey(AnalyticsAccountDataViewModel::class)
fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(StartAppViewModel::class)
fun startAppViewModelFactory(factory: StartAppViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)
@ -612,4 +619,9 @@ interface MavericksViewModelModule {
@IntoMap @IntoMap
@MavericksViewModelKey(FontScaleSettingViewModel::class) @MavericksViewModelKey(FontScaleSettingViewModel::class)
fun fontScaleSettingViewModelFactory(factory: FontScaleSettingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun fontScaleSettingViewModelFactory(factory: FontScaleSettingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(HomeRoomListViewModel::class)
fun homeRoomListViewModel(factory: HomeRoomListViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
} }

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.failure.MatrixIdFailure
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.failure.isMissingEmailVerification
import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@ -105,6 +106,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.message == "Not allowed to join this room" -> { throwable.error.message == "Not allowed to join this room" -> {
stringProvider.getString(R.string.room_error_access_unauthorized) stringProvider.getString(R.string.room_error_access_unauthorized)
} }
throwable.isMissingEmailVerification() -> {
stringProvider.getString(R.string.auth_reset_password_error_unverified)
}
else -> { else -> {
throwable.error.message.takeIf { it.isNotEmpty() } throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() }

View File

@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ActiveSessionSetter
import im.vector.app.core.network.WifiDetector import im.vector.app.core.network.WifiDetector
import im.vector.app.core.pushers.model.PushData import im.vector.app.core.pushers.model.PushData
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
@ -59,6 +60,7 @@ class VectorMessagingReceiver : MessagingReceiver() {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var notifiableEventResolver: NotifiableEventResolver @Inject lateinit var notifiableEventResolver: NotifiableEventResolver
@Inject lateinit var pushersManager: PushersManager @Inject lateinit var pushersManager: PushersManager
@Inject lateinit var activeSessionSetter: ActiveSessionSetter
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var vectorDataStore: VectorDataStore @Inject lateinit var vectorDataStore: VectorDataStore
@ -177,6 +179,11 @@ class VectorMessagingReceiver : MessagingReceiver() {
} }
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
?: run {
// Active session may not exist yet, if MainActivity has not been launched
activeSessionSetter.tryToSetActiveSession(startSync = false)
activeSessionHolder.getSafeActiveSession()
}
if (session == null) { if (session == null) {
Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") Timber.tag(loggerTag.value).w("## Can't sync from push, no current session")

View File

@ -0,0 +1,52 @@
/*
* 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 android.app.Activity
import android.content.pm.PackageManager
import android.webkit.PermissionRequest
import androidx.core.content.ContextCompat
import javax.inject.Inject
class CheckWebViewPermissionsUseCase @Inject constructor() {
/**
* Checks if required WebView permissions are already granted system level.
* @param activity the calling Activity that is requesting the permissions (or fragment parent)
* @param request WebView permission request of onPermissionRequest function
* @return true if WebView permissions are already granted, false otherwise
*/
fun execute(activity: Activity, request: PermissionRequest): Boolean {
return request.resources.all {
when (it) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> {
PERMISSIONS_FOR_AUDIO_IP_CALL.all { permission ->
ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED
}
}
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> {
PERMISSIONS_FOR_VIDEO_IP_CALL.all { permission ->
ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED
}
}
else -> {
false
}
}
}
}
}

View File

@ -17,11 +17,15 @@
package im.vector.app.features package im.vector.app.features
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.viewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -44,9 +48,16 @@ import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.session.VectorSessionStore import im.vector.app.features.session.VectorSessionStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.start.StartAppAction
import im.vector.app.features.start.StartAppAndroidService
import im.vector.app.features.start.StartAppViewEvent
import im.vector.app.features.start.StartAppViewModel
import im.vector.app.features.start.StartAppViewState
import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -73,6 +84,8 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
companion object { companion object {
private const val EXTRA_ARGS = "EXTRA_ARGS" private const val EXTRA_ARGS = "EXTRA_ARGS"
private const val EXTRA_NEXT_INTENT = "EXTRA_NEXT_INTENT"
private const val EXTRA_INIT_SESSION = "EXTRA_INIT_SESSION"
// Special action to clear cache and/or clear credentials // Special action to clear cache and/or clear credentials
fun restartApp(activity: Activity, args: MainActivityArgs) { fun restartApp(activity: Activity, args: MainActivityArgs) {
@ -82,8 +95,22 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
intent.putExtra(EXTRA_ARGS, args) intent.putExtra(EXTRA_ARGS, args)
activity.startActivity(intent) activity.startActivity(intent)
} }
fun getIntentToInitSession(activity: Activity): Intent {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra(EXTRA_INIT_SESSION, true)
return intent
}
fun getIntentWithNextIntent(context: Context, nextIntent: Intent): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(EXTRA_NEXT_INTENT, nextIntent)
return intent
}
} }
private val startAppViewModel: StartAppViewModel by viewModel()
override fun getBinding() = ActivityMainBinding.inflate(layoutInflater) override fun getBinding() = ActivityMainBinding.inflate(layoutInflater)
override fun getOtherThemes() = ActivityOtherThemes.Launcher override fun getOtherThemes() = ActivityOtherThemes.Launcher
@ -103,15 +130,58 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) { startAppViewModel.onEach {
clearNotifications() renderState(it)
} }
// Handle some wanted cleanup startAppViewModel.viewEvents.stream()
if (args.clearCache || args.clearCredentials) { .onEach(::handleViewEvents)
doCleanUp() .launchIn(lifecycleScope)
startAppViewModel.handle(StartAppAction.StartApp)
}
private fun renderState(state: StartAppViewState) {
if (state.mayBeLongToProcess) {
views.status.setText(R.string.updating_your_data)
}
views.status.isVisible = state.mayBeLongToProcess
}
private fun handleViewEvents(event: StartAppViewEvent) {
when (event) {
StartAppViewEvent.StartForegroundService -> handleStartForegroundService()
StartAppViewEvent.AppStarted -> handleAppStarted()
}
}
private fun handleStartForegroundService() {
if (startAppViewModel.shouldStartApp()) {
// Start foreground service, because the operation may take a while
val intent = Intent(this, StartAppAndroidService::class.java)
ContextCompat.startForegroundService(this, intent)
}
}
private fun handleAppStarted() {
if (intent.hasExtra(EXTRA_NEXT_INTENT)) {
// Start the next Activity
val nextIntent = intent.getParcelableExtra<Intent>(EXTRA_NEXT_INTENT)
startIntentAndFinish(nextIntent)
} else if (intent.hasExtra(EXTRA_INIT_SESSION)) {
setResult(RESULT_OK)
finish()
} else { } else {
startNextActivityAndFinish() args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) {
clearNotifications()
}
// Handle some wanted cleanup
if (args.clearCache || args.clearCredentials) {
doCleanUp()
} else {
startNextActivityAndFinish()
}
} }
} }
@ -241,7 +311,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
// We have a session. // We have a session.
// Check it can be opened // Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) { if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this, existingSession = true) HomeActivity.newIntent(this, firstStartMainActivity = false, existingSession = true)
} else { } else {
// The token is still invalid // The token is still invalid
navigator.softLogout(this) navigator.softLogout(this)
@ -253,6 +323,10 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
null null
} }
} }
startIntentAndFinish(intent)
}
private fun startIntentAndFinish(intent: Intent?) {
intent?.let { startActivity(it) } intent?.let { startActivity(it) }
finish() finish()
} }

View File

@ -46,9 +46,9 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingPersonalizeEnabled() = true
override fun isOnboardingCombinedRegisterEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = true
override fun isOnboardingCombinedLoginEnabled() = false override fun isOnboardingCombinedLoginEnabled() = true
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
override fun isScreenSharingEnabled(): Boolean = true override fun isScreenSharingEnabled(): Boolean = true
override fun forceUsageOfOpusEncoder(): Boolean = false override fun forceUsageOfOpusEncoder(): Boolean = false

View File

@ -16,7 +16,6 @@
package im.vector.app.features.analytics.accountdata package im.vector.app.features.analytics.accountdata
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -66,7 +65,7 @@ class AnalyticsAccountDataViewModel @AssistedInject constructor(
private fun observeInitSync() { private fun observeInitSync() {
combine( combine(
session.syncService().getSyncRequestStateLive().asFlow(), session.syncService().getSyncRequestStateFlow(),
analytics.getUserConsent(), analytics.getUserConsent(),
analytics.getAnalyticsId() analytics.getAnalyticsId()
) { status, userConsent, analyticsId -> ) { status, userConsent, analyticsId ->

View File

@ -604,7 +604,7 @@ class VectorCallActivity :
private fun returnToChat() { private fun returnToChat() {
val roomId = withState(callViewModel) { it.roomId } val roomId = withState(callViewModel) { it.roomId }
val args = TimelineArgs(roomId) val args = TimelineArgs(roomId)
val intent = RoomDetailActivity.newIntent(this, args).apply { val intent = RoomDetailActivity.newIntent(this, args, false).apply {
flags = FLAG_ACTIVITY_CLEAR_TOP flags = FLAG_ACTIVITY_CLEAR_TOP
} }
startActivity(intent) startActivity(intent)

View File

@ -86,6 +86,14 @@ class VerificationConclusionController @Inject constructor(
bottomGotIt() bottomGotIt()
} }
ConclusionState.INVALID_QR_CODE -> {
bottomSheetVerificationNoticeItem {
id("invalid_qr")
notice(host.stringProvider.getString(R.string.verify_invalid_qr_notice).toEpoxyCharSequence())
}
bottomGotIt()
}
ConclusionState.CANCELLED -> { ConclusionState.CANCELLED -> {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice_cancelled") id("notice_cancelled")

View File

@ -32,7 +32,8 @@ data class VerificationConclusionViewState(
enum class ConclusionState { enum class ConclusionState {
SUCCESS, SUCCESS,
WARNING, WARNING,
CANCELLED CANCELLED,
INVALID_QR_CODE
} }
class VerificationConclusionViewModel(initialState: VerificationConclusionViewState) : class VerificationConclusionViewModel(initialState: VerificationConclusionViewState) :
@ -44,7 +45,9 @@ class VerificationConclusionViewModel(initialState: VerificationConclusionViewSt
val args = viewModelContext.args<VerificationConclusionFragment.Args>() val args = viewModelContext.args<VerificationConclusionFragment.Args>()
return when (safeValueOf(args.cancelReason)) { return when (safeValueOf(args.cancelReason)) {
CancelCode.QrCodeInvalid, CancelCode.QrCodeInvalid -> {
VerificationConclusionViewState(ConclusionState.INVALID_QR_CODE, args.isMe)
}
CancelCode.MismatchedUser, CancelCode.MismatchedUser,
CancelCode.MismatchedSas, CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment, CancelCode.MismatchedCommitment,

View File

@ -78,6 +78,7 @@ import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.nightly.NightlyProxy
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -132,6 +133,7 @@ class HomeActivity :
@Inject lateinit var spaceStateHandler: SpaceStateHandler @Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy
private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> private val createSpaceResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) { if (activityResult.resultCode == Activity.RESULT_OK) {
@ -238,7 +240,8 @@ class HomeActivity :
homeActivityViewModel.observeViewEvents { homeActivityViewModel.observeViewEvents {
when (it) { when (it) {
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) is HomeActivityViewEvents.CurrentSessionNotVerified -> handleOnNewSession(it)
is HomeActivityViewEvents.CurrentSessionCannotBeVerified -> handleCantVerify(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
HomeActivityViewEvents.StartRecoverySetupFlow -> handleStartRecoverySetup() HomeActivityViewEvents.StartRecoverySetupFlow -> handleStartRecoverySetup()
is HomeActivityViewEvents.ForceVerification -> { is HomeActivityViewEvents.ForceVerification -> {
@ -422,7 +425,7 @@ class HomeActivity :
} }
} }
private fun handleOnNewSession(event: HomeActivityViewEvents.OnNewSession) { private fun handleOnNewSession(event: HomeActivityViewEvents.CurrentSessionNotVerified) {
// We need to ask // We need to ask
promptSecurityEvent( promptSecurityEvent(
event.userItem, event.userItem,
@ -437,6 +440,17 @@ class HomeActivity :
} }
} }
private fun handleCantVerify(event: HomeActivityViewEvents.CurrentSessionCannotBeVerified) {
// We need to ask
promptSecurityEvent(
event.userItem,
R.string.crosssigning_cannot_verify_this_session,
R.string.crosssigning_cannot_verify_this_session_desc
) {
it.navigator.open4SSetup(it, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
}
}
private fun handlePromptToEnablePush() { private fun handlePromptToEnablePush() {
popupAlertManager.postVectorAlert( popupAlertManager.postVectorAlert(
DefaultVectorAlert( DefaultVectorAlert(
@ -533,6 +547,9 @@ class HomeActivity :
// Force remote backup state update to update the banner if needed // Force remote backup state update to update the banner if needed
serverBackupStatusViewModel.refreshRemoteStateIfNeeded() serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
// Check nightly
nightlyProxy.onHomeResumed()
} }
override fun getMenuRes() = R.menu.home override fun getMenuRes() = R.menu.home
@ -611,6 +628,7 @@ class HomeActivity :
companion object { companion object {
fun newIntent( fun newIntent(
context: Context, context: Context,
firstStartMainActivity: Boolean,
clearNotification: Boolean = false, clearNotification: Boolean = false,
authenticationDescription: AuthenticationDescription? = null, authenticationDescription: AuthenticationDescription? = null,
existingSession: Boolean = false, existingSession: Boolean = false,
@ -623,10 +641,16 @@ class HomeActivity :
inviteNotificationRoomId = inviteNotificationRoomId inviteNotificationRoomId = inviteNotificationRoomId
) )
return Intent(context, HomeActivity::class.java) val intent = Intent(context, HomeActivity::class.java)
.apply { .apply {
putExtra(Mavericks.KEY_ARG, args) putExtra(Mavericks.KEY_ARG, args)
} }
return if (firstStartMainActivity) {
MainActivity.getIntentWithNextIntent(context, intent)
} else {
intent
}
} }
} }

View File

@ -21,7 +21,13 @@ import org.matrix.android.sdk.api.util.MatrixItem
sealed interface HomeActivityViewEvents : VectorViewEvents { sealed interface HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents
data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents data class CurrentSessionNotVerified(
val userItem: MatrixItem.UserItem?,
val waitForIncomingRequest: Boolean = true,
) : HomeActivityViewEvents
data class CurrentSessionCannotBeVerified(
val userItem: MatrixItem.UserItem?,
) : HomeActivityViewEvents
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents

View File

@ -16,7 +16,6 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -218,8 +217,7 @@ class HomeActivityViewModel @AssistedInject constructor(
private fun observeInitialSync() { private fun observeInitialSync() {
val session = activeSessionHolder.getSafeActiveSession() ?: return val session = activeSessionHolder.getSafeActiveSession() ?: return
session.syncService().getSyncRequestStateLive() session.syncService().getSyncRequestStateFlow()
.asFlow()
.onEach { status -> .onEach { status ->
when (status) { when (status) {
is SyncRequestState.Idle -> { is SyncRequestState.Idle -> {
@ -364,14 +362,30 @@ class HomeActivityViewModel @AssistedInject constructor(
// If 4S is forced, force verification // If 4S is forced, force verification
_viewEvents.post(HomeActivityViewEvents.ForceVerification(true)) _viewEvents.post(HomeActivityViewEvents.ForceVerification(true))
} else { } else {
// New session // we wan't to check if there is a way to actually verify this session,
_viewEvents.post( // that means that there is another session to verify against, or
HomeActivityViewEvents.OnNewSession( // secure backup is setup
session.getUser(session.myUserId)?.toMatrixItem(), val hasTargetDeviceToVerifyAgainst = session
// Always send request instead of waiting for an incoming as per recent EW changes .cryptoService()
false .getUserDevices(session.myUserId)
) .size >= 2 // this one + another
) val is4Ssetup = session.sharedSecretStorageService().isRecoverySetup()
if (hasTargetDeviceToVerifyAgainst || is4Ssetup) {
// New session
_viewEvents.post(
HomeActivityViewEvents.CurrentSessionNotVerified(
session.getUser(session.myUserId)?.toMatrixItem(),
// Always send request instead of waiting for an incoming as per recent EW changes
false
)
)
} else {
_viewEvents.post(
HomeActivityViewEvents.CurrentSessionCannotBeVerified(
session.getUser(session.myUserId)?.toMatrixItem(),
)
)
}
} }
} }
} }

View File

@ -41,6 +41,7 @@ import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.dialpad.DialPadFragment
@ -48,6 +49,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorLocale
@ -66,7 +68,8 @@ class HomeDetailFragment @Inject constructor(
private val alertManager: PopupAlertManager, private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val spaceStateHandler: SpaceStateHandler private val spaceStateHandler: SpaceStateHandler,
private val vectorFeatures: VectorFeatures,
) : VectorBaseFragment<FragmentHomeDetailBinding>(), ) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate, KeysBackupBanner.Delegate,
CurrentCallsView.Callback, CurrentCallsView.Callback,
@ -352,8 +355,12 @@ class HomeDetailFragment @Inject constructor(
if (fragmentToShow == null) { if (fragmentToShow == null) {
when (tab) { when (tab) {
is HomeTab.RoomList -> { is HomeTab.RoomList -> {
val params = RoomListParams(tab.displayMode) if (vectorFeatures.isNewAppLayoutEnabled()) {
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag)
} else {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
} }
is HomeTab.DialPad -> { is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag) add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)

View File

@ -198,8 +198,7 @@ class HomeDetailViewModel @AssistedInject constructor(
copy(syncState = syncState) copy(syncState = syncState)
} }
session.syncService().getSyncRequestStateLive() session.syncService().getSyncRequestStateFlow()
.asFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncRequestState>() .filterIsInstance<SyncRequestState.IncrementalSyncRequestState>()
.setOnEach { .setOnEach {
copy(incrementalSyncRequestState = it) copy(incrementalSyncRequestState = it)

View File

@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Live Location // Live Location
object StopLiveLocationSharing : RoomDetailAction() object StopLiveLocationSharing : RoomDetailAction()
object OpenElementCallWidget : RoomDetailAction()
} }

View File

@ -35,6 +35,7 @@ import im.vector.app.core.extensions.keepScreenOn
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.databinding.ActivityRoomDetailBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
@ -191,10 +192,15 @@ class RoomDetailActivity :
const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID"
const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT"
fun newIntent(context: Context, timelineArgs: TimelineArgs): Intent { fun newIntent(context: Context, timelineArgs: TimelineArgs, firstStartMainActivity: Boolean): Intent {
return Intent(context, RoomDetailActivity::class.java).apply { val intent = Intent(context, RoomDetailActivity::class.java).apply {
putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs) putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs)
} }
return if (firstStartMainActivity) {
MainActivity.getIntentWithNextIntent(context, intent)
} else {
intent
}
} }
// Shortcuts can't have intents with parcelables // Shortcuts can't have intents with parcelables

View File

@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents()
object RoomReplacementStarted : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents()
object OpenElementCallWidget : RoomDetailViewEvents()
} }

View File

@ -102,6 +102,8 @@ data class RoomDetailViewState(
// It can differs for a short period of time on the JitsiState as its computed async. // It can differs for a short period of time on the JitsiState as its computed async.
fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse()
fun hasActiveElementCallWidget() = activeRoomWidgets()?.any { it.type == WidgetType.ElementCall && it.isActive }.orFalse()
fun isDm() = asyncRoomSummary()?.isDirect == true fun isDm() = asyncRoomSummary()?.isDirect == true
fun isThreadTimeline() = rootThreadEventId != null fun isThreadTimeline() = rootThreadEventId != null

View File

@ -47,6 +47,11 @@ class StartCallActionsHandler(
} }
private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state -> private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state ->
if (state.hasActiveElementCallWidget() && !isVideoCall) {
timelineViewModel.handle(RoomDetailAction.OpenElementCallWidget)
return@withState
}
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
when (roomSummary.joinedMembersCount) { when (roomSummary.joinedMembersCount) {
1 -> { 1 -> {

View File

@ -498,6 +498,7 @@ class TimelineFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
} }
} }
@ -859,6 +860,9 @@ class TimelineFragment @Inject constructor(
views.locationLiveStatusIndicator.stopButton.debouncedClicks { views.locationLiveStatusIndicator.stopButton.debouncedClicks {
timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing) timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing)
} }
views.locationLiveStatusIndicator.debouncedClicks {
navigateToLocationLiveMap()
}
} }
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
@ -1090,9 +1094,8 @@ class TimelineFragment @Inject constructor(
2 -> state.isAllowedToStartWebRTCCall 2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets else -> state.isAllowedToManageWidgets
} }
setOf(R.id.voice_call, R.id.video_call).forEach { menu.findItem(R.id.video_call).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40
menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 menu.findItem(R.id.voice_call).icon?.alpha = if (callButtonsEnabled || state.hasActiveElementCallWidget()) 0xFF else 0x40
}
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
@ -1206,9 +1209,9 @@ class TimelineFragment @Inject constructor(
getRootThreadEventId()?.let { getRootThreadEventId()?.let {
val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it) val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it)
context?.let { con -> context?.let { con ->
val int = RoomDetailActivity.newIntent(con, newRoom) val intent = RoomDetailActivity.newIntent(con, newRoom, false)
int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
con.startActivity(int) con.startActivity(intent)
} }
} }
} }
@ -1257,7 +1260,7 @@ class TimelineFragment @Inject constructor(
val nonFormattedBody = when (messageContent) { val nonFormattedBody = when (messageContent) {
is MessageAudioContent -> getAudioContentBodyText(messageContent) is MessageAudioContent -> getAudioContentBodyText(messageContent)
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
is MessageBeaconInfoContent -> getString(R.string.sent_live_location) is MessageBeaconInfoContent -> getString(R.string.live_location_description)
else -> messageContent?.body.orEmpty() else -> messageContent?.body.orEmpty()
} }
var formattedBody: CharSequence? = null var formattedBody: CharSequence? = null
@ -2653,6 +2656,15 @@ class TimelineFragment @Inject constructor(
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET") .show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET")
} }
private fun handleOpenElementCallWidget() = withState(timelineViewModel) { state ->
state
.activeRoomWidgets()
?.find { it.type == WidgetType.ElementCall }
?.also { widget ->
navigator.openRoomWidget(requireContext(), state.roomId, widget)
}
}
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View File

@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
@ -467,6 +466,13 @@ class TimelineViewModel @AssistedInject constructor(
} }
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing()
RoomDetailAction.OpenElementCallWidget -> handleOpenElementCallWidget()
}
}
private fun handleOpenElementCallWidget() = withState { state ->
if (state.hasActiveElementCallWidget()) {
_viewEvents.post(RoomDetailViewEvents.OpenElementCallWidget)
} }
} }
@ -752,7 +758,7 @@ class TimelineViewModel @AssistedInject constructor(
R.id.timeline_setting -> true R.id.timeline_setting -> true
R.id.invite -> state.canInvite R.id.invite -> state.canInvite
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call -> state.isCallOptionAvailable() R.id.voice_call -> state.isCallOptionAvailable() || state.hasActiveElementCallWidget()
R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
// Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
@ -1145,8 +1151,7 @@ class TimelineViewModel @AssistedInject constructor(
copy(syncState = syncState) copy(syncState = syncState)
} }
session.syncService().getSyncRequestStateLive() session.syncService().getSyncRequestStateFlow()
.asFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncRequestState>() .filterIsInstance<SyncRequestState.IncrementalSyncRequestState>()
.setOnEach { .setOnEach {
copy(incrementalSyncRequestState = it) copy(incrementalSyncRequestState = it)

View File

@ -102,7 +102,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
runningState: LiveLocationShareViewState.Running, runningState: LiveLocationShareViewState.Running,
): MessageLiveLocationItem { ): MessageLiveLocationItem {
// TODO only render location if enabled in preferences: to be handled in a next PR
val width = timelineMediaSizeProvider.getMaxSize().first val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)

View File

@ -19,7 +19,10 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R import im.vector.app.R
@ -50,8 +53,8 @@ class DefaultLiveLocationShareStatusItem : LiveLocationShareStatusItem {
height = mapHeight height = mapHeight
} }
GlideApp.with(mapImageView) GlideApp.with(mapImageView)
.load(R.drawable.bg_no_location_map) .load(ContextCompat.getDrawable(mapImageView.context, R.drawable.bg_no_location_map))
.transform(mapCornerTransformation) .transform(MultiTransformation(CenterCrop(), mapCornerTransformation))
.into(mapImageView) .into(mapImageView)
} }

View File

@ -42,7 +42,7 @@ abstract class MessageLiveLocationInactiveItem :
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val bannerImageView by bind<ImageView>(R.id.locationLiveInactiveBanner) val bannerImageView by bind<ImageView>(R.id.locationLiveEndedBannerBackground)
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveInactiveMap) val noLocationMapImageView by bind<ImageView>(R.id.locationLiveInactiveMap)
} }

View File

@ -26,8 +26,8 @@ import im.vector.app.core.resources.toTimestamp
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.location.live.LocationLiveMessageBannerView
import im.vector.app.features.location.live.LocationLiveMessageBannerViewState import im.vector.app.features.location.live.LocationLiveMessageBannerViewState
import im.vector.app.features.location.live.LocationLiveRunningBannerView
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
@EpoxyModelClass @EpoxyModelClass
@ -52,9 +52,9 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
val isEmitter = currentUserId != null && currentUserId == locationUserId val isEmitter = currentUserId != null && currentUserId == locationUserId
val messageLayout = attributes.informationData.messageLayout val messageLayout = attributes.informationData.messageLayout
val viewState = buildViewState(holder, messageLayout, isEmitter) val viewState = buildViewState(holder, messageLayout, isEmitter)
holder.locationLiveMessageBanner.isVisible = true holder.locationLiveRunningBanner.isVisible = true
holder.locationLiveMessageBanner.render(viewState) holder.locationLiveRunningBanner.render(viewState)
holder.locationLiveMessageBanner.stopButton.setOnClickListener { holder.locationLiveRunningBanner.stopButton.setOnClickListener {
attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing) attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing)
} }
} }
@ -112,7 +112,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageLocationItem.Holder(STUB_ID) { class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
val locationLiveMessageBanner by bind<LocationLiveMessageBannerView>(R.id.locationLiveMessageBanner) val locationLiveRunningBanner by bind<LocationLiveRunningBannerView>(R.id.locationLiveRunningBanner)
} }
companion object { companion object {

View File

@ -331,7 +331,7 @@ class RoomListSectionBuilder(
}, },
{ queryParams -> { queryParams ->
val name = stringProvider.getString(R.string.bottom_action_rooms) val name = stringProvider.getString(R.string.bottom_action_rooms)
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams, getFlattenParents = true) val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams)
onUpdatable(updatableFilterLivePageResult) onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View File

@ -207,9 +207,18 @@ class RoomSummaryItemFactory @Inject constructor(
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String { private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
val userId = roomSummary.directUserId val userId = roomSummary.directUserId
val spaceName = roomSummary.flattenParents.lastOrNull()?.name val directParent = joinParentNames(roomSummary)
val canonicalAlias = roomSummary.canonicalAlias val canonicalAlias = roomSummary.canonicalAlias
return (userId ?: spaceName ?: canonicalAlias).orEmpty() return (userId ?: directParent ?: canonicalAlias).orEmpty()
}
private fun joinParentNames(roomSummary: RoomSummary) = with(roomSummary) {
when (val size = directParentNames.size) {
0 -> null
1 -> directParentNames.first()
2 -> stringProvider.getString(R.string.search_space_two_parents, directParentNames[0], directParentNames[1])
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
}
} }
} }

View File

@ -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.app.features.home.room.list.home
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
sealed class HomeRoomListAction : VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary) : HomeRoomListAction()
data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : HomeRoomListAction()
data class ToggleTag(val roomId: String, val tag: String) : HomeRoomListAction()
data class LeaveRoom(val roomId: String) : HomeRoomListAction()
}

View File

@ -0,0 +1,226 @@
/*
* 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.home.room.list.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyControllerAdapter
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.databinding.FragmentRoomListBinding
import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListAnimator
import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryPagedController
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import javax.inject.Inject
class HomeRoomListFragment @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val userPreferencesProvider: UserPreferencesProvider
) : VectorBaseFragment<FragmentRoomListBinding>(),
RoomListListener {
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
private var concatAdapter = ConcatAdapter()
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var stateRestorer: LayoutManagerStateRestorer
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomListBinding {
return FragmentRoomListBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
sharedActionViewModel
.stream()
.onEach { handleQuickActions(it) }
.launchIn(viewLifecycleOwner.lifecycleScope)
views.stateView.contentView = views.roomListView
views.stateView.state = StateView.State.Loading
roomListViewModel.observeViewEvents {
when (it) {
is HomeRoomListViewEvents.Loading -> showLoading(it.message)
is HomeRoomListViewEvents.Failure -> showFailure(it.throwable)
is HomeRoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
is HomeRoomListViewEvents.Done -> Unit
}
}
setupRecyclerView()
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
views.roomListView.layoutManager = layoutManager
views.roomListView.itemAnimator = RoomListAnimator()
layoutManager.recycleChildrenOnDetach = true
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomListViewModel.sections.onEach { sections ->
setUpAdapters(sections)
}.launchIn(lifecycleScope)
views.roomListView.adapter = concatAdapter
}
override fun invalidate() = withState(roomListViewModel) { state ->
views.stateView.state = state.state
}
private fun setUpAdapters(sections: Set<HomeRoomSection>) {
sections.forEach {
concatAdapter.addAdapter(getAdapterForData(it))
}
}
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
}
is RoomListQuickActionsSharedAction.NotificationsAll -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES))
}
is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY))
}
is RoomListQuickActionsSharedAction.NotificationsMute -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE))
}
is RoomListQuickActionsSharedAction.Settings -> {
navigator.openRoomProfile(requireActivity(), quickAction.roomId)
}
is RoomListQuickActionsSharedAction.Favorite -> {
roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE))
}
is RoomListQuickActionsSharedAction.LowPriority -> {
roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY))
}
is RoomListQuickActionsSharedAction.Leave -> {
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(quickAction.roomId))
promptLeaveRoom(quickAction.roomId)
}
}
}
private fun promptLeaveRoom(roomId: String) {
val isPublicRoom = roomListViewModel.isPublicRoom(roomId)
val message = buildString {
append(getString(R.string.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(R.string.room_participants_leave_private_warning))
}
}
MaterialAlertDialogBuilder(requireContext(), if (isPublicRoom) 0 else R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setTitle(R.string.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(R.string.action_leave) { _, _ ->
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId))
}
.setNegativeButton(R.string.action_cancel, null)
.show()
}
private fun getAdapterForData(data: HomeRoomSection): EpoxyControllerAdapter {
return when (data) {
is HomeRoomSection.RoomSummaryData -> {
RoomSummaryPagedController(
roomSummaryItemFactory,
RoomListDisplayMode.ROOMS
).also { controller ->
controller.listener = this
data.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}.adapter
}
}
}
private fun handleSelectRoom(event: HomeRoomListViewEvents.SelectRoom, isInviteAlreadyAccepted: Boolean) {
navigator.openRoom(
context = requireActivity(),
roomId = event.roomSummary.roomId,
isInviteAlreadyAccepted = isInviteAlreadyAccepted,
trigger = ViewRoom.Trigger.RoomList
)
}
// region RoomListListener
override fun onRoomClicked(room: RoomSummary) {
roomListViewModel.handle(HomeRoomListAction.SelectRoom(room))
}
override fun onRoomLongClicked(room: RoomSummary): Boolean {
userPreferencesProvider.neverShowLongClickOnRoomHelpAgain()
RoomListQuickActionsBottomSheet
.newInstance(room.roomId)
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
return true
}
override fun onRejectRoomInvitation(room: RoomSummary) {
TODO("Not yet implemented")
}
override fun onAcceptRoomInvitation(room: RoomSummary) {
TODO("Not yet implemented")
}
override fun onJoinSuggestedRoom(room: SpaceChildInfo) {
TODO("Not yet implemented")
}
override fun onSuggestedRoomClicked(room: SpaceChildInfo) {
TODO("Not yet implemented")
}
// endregion
}

View File

@ -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.features.home.room.list.home
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class HomeRoomListViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : HomeRoomListViewEvents()
data class Failure(val throwable: Throwable) : HomeRoomListViewEvents()
object Done : HomeRoomListViewEvents()
data class SelectRoom(val roomSummary: RoomSummary, val isInviteAlreadyAccepted: Boolean = false) : HomeRoomListViewEvents()
}

Some files were not shown because too many files have changed in this diff Show More