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
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak
yes n | towncrier --version nightly
yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly APK
run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES --stacktrace

View file

@ -248,9 +248,12 @@ jobs:
# Skip in forks
if: >
github.repository == 'vector-im/element-android' &&
(contains(github.event.issue.labels.*.name, 'Z-ElementX-Alpha') ||
contains(github.event.issue.labels.*.name, 'Z-ElementX-Beta') ||
contains(github.event.issue.labels.*.name, 'Z-ElementX'))
(contains(github.event.issue.labels.*.name, 'Z-BBQ-Alpha') ||
contains(github.event.issue.labels.*.name, 'Z-BBQ-Beta') ||
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:
- uses: octokit/graphql-action@v2.x
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 kotlinCoroutines = "1.6.4"
def dagger = "2.42"
def appDistribution = "16.0.0-beta03"
def retrofit = "2.9.0"
def arrow = "0.8.2"
def markwon = "4.6.2"
@ -49,9 +50,7 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
'annotation' : "androidx.annotation:annotation:1.4.0",
'activity' : "androidx.activity:activity:1.5.0",
'annotations' : "androidx.annotation:annotation:1.3.0",
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.8.0",
@ -83,7 +82,9 @@ ext.libs = [
'transition' : "androidx.transition:transition:1.2.0",
],
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' : "com.google.dagger:dagger:$dagger",

Binary file not shown.

View file

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

6
gradlew vendored
View file

@ -205,6 +205,12 @@ set -- \
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.
#
# 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
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
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 org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -47,7 +46,6 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class WithHeldTests : InstrumentedTest {
@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 &&
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
*/

View file

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

View file

@ -87,7 +87,10 @@ object EventType {
// Key share events
const val ROOM_KEY_REQUEST = "m.room_key_request"
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 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 pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
* @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(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
getFlattenParents: Boolean = false,
): UpdatableLivePageResult
/**

View file

@ -164,9 +164,9 @@ data class RoomSummary(
*/
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.
*/

View file

@ -60,9 +60,9 @@ interface SyncService {
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.

View file

@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy {
WidgetType.StickerPicker,
WidgetType.Grafana,
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 Custom : WidgetType("m.custom")
object IntegrationManager : WidgetType("m.integration_manager")
object ElementCall : WidgetType("io.element.call")
data class Fallback(override val preferred: String) : WidgetType(preferred)
fun matches(type: String): Boolean {

View file

@ -21,4 +21,14 @@ interface BuildVersionSdkIntProvider {
* Return the current version of the Android SDK.
*/
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 -> {
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
in EventType.ROOM_KEY_WITHHELD.values -> {
onKeyWithHeldReceived(event)
}
else -> {
@ -869,7 +869,7 @@ internal class DefaultCryptoService @Inject constructor(
senderKey = withHeldContent.senderKey,
fromDevice = withHeldContent.fromDevice,
event = Event(
type = EventType.ROOM_KEY_WITHHELD,
type = EventType.ROOM_KEY_WITHHELD.stable,
senderId = senderId,
content = event.getClearContent()
)

View file

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

View file

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

View file

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

View file

@ -84,7 +84,7 @@ internal class DefaultQrCodeVerificationTransaction(
// Perform some checks
if (otherQrCodeData.transactionId != transactionId) {
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
cancel(CancelCode.QrCodeInvalid)
cancel(CancelCode.UnknownTransaction)
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.MigrateSessionTo033
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.database.MatrixRealmMigration
import javax.inject.Inject
@ -59,7 +60,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 34L,
schemaVersion = 35L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -103,5 +104,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 32) MigrateSessionTo032(realm).perform()
if (oldVersion < 33) MigrateSessionTo033(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
)
},
directParentNames = roomSummaryEntity.directParentNames.toList(),
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
// 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 = "",
var roomType: String? = null,
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
var children: RealmList<SpaceChildSummaryEntity> = RealmList()
var children: RealmList<SpaceChildSummaryEntity> = RealmList(),
var directParentNames: RealmList<String> = RealmList(),
) : RealmObject() {
private var displayName: String? = ""

View file

@ -152,9 +152,8 @@ internal class DefaultRoomService @Inject constructor(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder,
getFlattenParents: Boolean
): UpdatableLivePageResult {
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents)
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
}
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_IMAGE -> return TextContent("sent an image.")
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 -> {
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
}

View file

@ -200,14 +200,13 @@ internal class RoomSummaryDataSource @Inject constructor(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder,
getFlattenedParents: Boolean = false
): UpdatableLivePageResult {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams).process(sortOrder)
}
val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it)
}.map { if (getFlattenedParents) it.getWithParents() else it }
}
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> {
val liveRooms = monarchy.findAllManagedWithChanges {
roomSummariesQuery(it, queryParams)

View file

@ -223,6 +223,7 @@ internal class RoomSummaryUpdater @Inject constructor(
.sort(RoomSummaryEntityFields.ROOM_ID)
.findAll().map {
it.flattenParentIds = null
it.directParentNames.clear()
it to emptyList<RoomSummaryEntity>().toMutableSet()
}
.toMap()
@ -350,39 +351,29 @@ internal class RoomSummaryUpdater @Inject constructor(
}
val acyclicGraph = graph.withoutEdges(backEdges)
// Timber.v("## SPACES: acyclicGraph $acyclicGraph")
val flattenSpaceParents = acyclicGraph.flattenDestination().map {
it.key.name to it.value.map { it.name }
}.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
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
.forEach { entry ->
val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
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)
// Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}")
entry.value.forEach { child ->
RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
childSum.directParentNames.add(parent.displayName())
// Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}")
// var allParents = childSum.flattenParentIds ?: ""
if (childSum.flattenParentIds == null) childSum.flattenParentIds = ""
if (childSum.flattenParentIds == null) {
childSum.flattenParentIds = ""
}
flattenParentsIds.forEach {
if (childSum.flattenParentIds?.contains(it) != true) {
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
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.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
@ -75,9 +73,7 @@ internal class DefaultSyncService @Inject constructor(
override fun getSyncState() = getSyncThread().currentState()
override fun getSyncRequestStateLive(): LiveData<SyncRequestState> {
return syncRequestStateTracker.syncRequestState
}
override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null

View file

@ -16,23 +16,26 @@
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.SyncRequestState
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class SyncRequestStateTracker @Inject constructor() :
ProgressReporter {
internal class SyncRequestStateTracker @Inject constructor(
private val coroutineScope: CoroutineScope
) : ProgressReporter {
val syncRequestState = MutableLiveData<SyncRequestState>()
val syncRequestState = MutableSharedFlow<SyncRequestState>()
private var rootTask: TaskInfo? = null
// Only to be used for incremental sync
fun setSyncRequestState(newSyncRequestState: SyncRequestState.IncrementalSyncRequestState) {
syncRequestState.postValue(newSyncRequestState)
emitSyncState(newSyncRequestState)
}
/**
@ -42,7 +45,9 @@ internal class SyncRequestStateTracker @Inject constructor() :
initialSyncStep: InitialSyncStep,
totalProgress: Int
) {
endAll()
if (rootTask != null) {
endAll()
}
rootTask = TaskInfo(initialSyncStep, totalProgress, null, 1F)
reportProgress(0F)
}
@ -71,7 +76,7 @@ internal class SyncRequestStateTracker @Inject constructor() :
// Update the progress of the leaf and all its parents
leaf.setProgress(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
endedTask.parent.child = null
} else {
syncRequestState.postValue(SyncRequestState.Idle)
emitSyncState(SyncRequestState.Idle)
}
}
}
fun endAll() {
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.mavericks
// Nightly
// API-only library
gplayImplementation libs.google.appdistributionApi
// Full SDK implementation
gplayImplementation libs.google.appdistribution
// 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.BIOMETRIC_ERROR_NONE_ENROLLED
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.TestBuildVersionSdkIntProvider
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.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.tests.LockScreenTestActivity
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.Ignore
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.TimeUnit
@ -64,6 +70,13 @@ class BiometricHelperTests {
private val biometricManager = mockk<BiometricManager>(relaxed = true)
private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true)
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
fun setup() {
@ -188,8 +201,10 @@ class BiometricHelperTests {
}
@OptIn(ExperimentalCoroutinesApi::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
@Test
fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
mockkStatic("kotlinx.coroutines.flow.FlowKt")
val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) {
// Empty flow to keep the dialog open
@ -201,6 +216,9 @@ class BiometricHelperTests {
mockkObject(DevicePromptCheck)
every { DevicePromptCheck.isDeviceWithNoBiometricUI } 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 intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
with(ActivityScenario.launch<LockScreenTestActivity>(intent)) {
@ -214,11 +232,13 @@ class BiometricHelperTests {
}
}
latch.await(1, TimeUnit.SECONDS)
keyStore.deleteEntry(keyAlias)
unmockkObject(DevicePromptCheck)
unmockkStatic("kotlinx.coroutines.flow.FlowKt")
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
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 secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider))
private val keyStoreCrypto = spyk(
KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils)
KeyStoreCrypto(alias, false, context, versionProvider, keyStore).also {
it.secretStoringUtils = secretStoringUtils
}
)
@After
@ -146,10 +148,10 @@ class KeyStoreCryptoTests {
@Test
fun getCryptoObjectUsesCipherFromSecretStoringUtils() {
keyStoreCrypto.getCryptoObject()
keyStoreCrypto.getAuthCryptoObject()
verify { secretStoringUtils.getEncryptCipher(any()) }
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
import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.settings.VectorPreferences
import io.mockk.clearAllMocks
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
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.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldNotThrow
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -49,7 +44,7 @@ class LockScreenKeyRepositoryTests {
}
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 keyStore: KeyStore by lazy {
@ -58,7 +53,7 @@ class LockScreenKeyRepositoryTests {
@Before
fun setup() {
lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory))
lockScreenKeyRepository = spyk(LockScreenKeyRepository("base.pin_code", "base.system", keyStoreCryptoFactory))
}
@After
@ -141,44 +136,4 @@ class LockScreenKeyRepositoryTests {
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")
package im.vector.app.features.pin.lockscreen.crypto
package im.vector.app.features.pin.lockscreen.crypto.migrations
import android.os.Build
import android.security.KeyPairGeneratorSpec
@ -57,7 +57,7 @@ import javax.crypto.spec.PSource
import javax.security.auth.x500.X500Principal
import kotlin.math.abs
class PinCodeMigratorTests {
class LegacyPinCodeMigratorTests {
private val alias = UUID.randomUUID().toString()
@ -72,7 +72,9 @@ class PinCodeMigratorTests {
private val secretStoringUtils: SecretStoringUtils = spyk(
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
)
private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider))
private val legacyPinCodeMigrator = spyk(
LegacyPinCodeMigrator(alias, pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)
)
@After
fun tearDown() {
@ -87,21 +89,21 @@ class PinCodeMigratorTests {
@Test
fun isMigrationNeededReturnsTrueIfLegacyKeyExists() {
pinCodeMigrator.isMigrationNeeded() shouldBe false
legacyPinCodeMigrator.isMigrationNeeded() shouldBe false
generateLegacyKey()
pinCodeMigrator.isMigrationNeeded() shouldBe true
legacyPinCodeMigrator.isMigrationNeeded() shouldBe true
}
@Test
fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false
every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
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()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -109,13 +111,13 @@ class PinCodeMigratorTests {
@Test
fun migrateWillReturnEarlyIfIsNotNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false
coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234"
every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
coEvery { legacyPinCodeMigrator.getDecryptedPinCode() } returns "1234"
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()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -126,9 +128,9 @@ class PinCodeMigratorTests {
val pinCode = "1234"
saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias)
legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() }
coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) }
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -145,9 +147,9 @@ class PinCodeMigratorTests {
every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias)
legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() }
coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) }
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.click
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.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
@ -182,13 +181,8 @@ class ElementRobot {
val activity = EspressoHelper.getCurrentActivity()!!
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)!!
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))
pressBack()
}.onFailure { Timber.w(it, "Verification popup missing") }
}

View file

@ -34,31 +34,46 @@ import im.vector.app.waitForView
class OnboardingRobot {
private val defaultVectorFeatures = DefaultVectorFeatures()
fun crawl() {
waitUntilViewVisible(withId(R.id.loginSplashSubmit))
crawlGetStarted()
crawlCreateAccount()
crawlAlreadyHaveAccount()
}
private fun crawlGetStarted() {
clickOn(R.id.loginSplashSubmit)
assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title)
clickOn(R.id.useCaseOptionOne)
OnboardingServersRobot().crawlSignUp()
pressBack()
pressBack()
private fun crawlCreateAccount() {
if (defaultVectorFeatures.isOnboardingCombinedRegisterEnabled()) {
// TODO https://github.com/vector-im/element-android/issues/6652
} else {
clickOn(R.id.loginSplashSubmit)
assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title)
clickOn(R.id.useCaseOptionOne)
OnboardingServersRobot().crawlSignUp()
pressBack()
pressBack()
}
}
private fun crawlAlreadyHaveAccount() {
clickOn(R.id.loginSplashAlreadyHaveAccount)
OnboardingServersRobot().crawlSignIn()
pressBack()
if (defaultVectorFeatures.isOnboardingCombinedLoginEnabled()) {
// TODO https://github.com/vector-im/element-android/issues/6652
} else {
clickOn(R.id.loginSplashAlreadyHaveAccount)
OnboardingServersRobot().crawlSignIn()
pressBack()
}
}
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))
if (DefaultVectorFeatures().isOnboardingPersonalizeEnabled()) {
if (defaultVectorFeatures.isOnboardingPersonalizeEnabled()) {
clickOn(R.string.ftue_account_created_personalize)
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") {
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(

View file

@ -17,13 +17,12 @@
package im.vector.app.ui.robot.settings
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.settings.font.FontScaleSettingActivity
class SettingsPreferencesRobot {
@ -34,8 +33,7 @@ class SettingsPreferencesRobot {
clickOn(R.string.settings_theme)
clickDialogNegativeButton()
clickOn(R.string.font_size)
waitUntilActivityVisible<FontScaleSettingActivity> {
pressBack()
}
waitUntilViewVisible(withId(R.id.fons_scale_recycler))
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.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.analytics.ui.consent.AnalyticsOptInActivity" />
@ -380,6 +381,11 @@
android:exported="false"
android:foregroundServiceType="location" />
<service
android:name=".features.start.StartAppAndroidService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".features.call.webrtc.ScreenCaptureAndroidService"
android:exported="false"

View file

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

View file

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

View file

@ -41,8 +41,6 @@ import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
import dagger.hilt.android.HiltAndroidApp
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration
@ -165,14 +163,6 @@ class VectorApplication :
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 {
override fun onResume(owner: LifecycleOwner) {
Timber.i("App entered foreground")
@ -205,14 +195,6 @@ class VectorApplication :
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() {
if (BuildConfig.ENABLE_STRICT_MODE_LOGS) {
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.search.SearchFragment
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.location.LocationPreviewFragment
import im.vector.app.features.location.LocationSharingFragment
@ -1041,4 +1042,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(LocationPreviewFragment::class)
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.upgrade.MigrateRoomViewModel
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.invite.InviteUsersToRoomViewModel
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.preview.SpacePreviewViewModel
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.usercode.UserCodeSharedViewModel
import im.vector.app.features.userdirectory.UserListViewModel
@ -483,6 +485,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(AnalyticsAccountDataViewModel::class)
fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(StartAppViewModel::class)
fun startAppViewModelFactory(factory: StartAppViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)
@ -612,4 +619,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(FontScaleSettingViewModel::class)
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.isInvalidPassword
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 java.net.HttpURLConnection
import java.net.SocketTimeoutException
@ -105,6 +106,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.message == "Not allowed to join this room" -> {
stringProvider.getString(R.string.room_error_access_unauthorized)
}
throwable.isMissingEmailVerification() -> {
stringProvider.getString(R.string.auth_reset_password_error_unverified)
}
else -> {
throwable.error.message.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 im.vector.app.BuildConfig
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.pushers.model.PushData
import im.vector.app.core.services.GuardServiceStarter
@ -59,6 +60,7 @@ class VectorMessagingReceiver : MessagingReceiver() {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var notifiableEventResolver: NotifiableEventResolver
@Inject lateinit var pushersManager: PushersManager
@Inject lateinit var activeSessionSetter: ActiveSessionSetter
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var vectorDataStore: VectorDataStore
@ -177,6 +179,11 @@ class VectorMessagingReceiver : MessagingReceiver() {
}
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) {
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
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.viewModel
import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.settings.VectorPreferences
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.ui.UiStateRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ -73,6 +84,8 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
companion object {
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
fun restartApp(activity: Activity, args: MainActivityArgs) {
@ -82,8 +95,22 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
intent.putExtra(EXTRA_ARGS, args)
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 getOtherThemes() = ActivityOtherThemes.Launcher
@ -103,15 +130,58 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) {
clearNotifications()
startAppViewModel.onEach {
renderState(it)
}
// Handle some wanted cleanup
if (args.clearCache || args.clearCredentials) {
doCleanUp()
startAppViewModel.viewEvents.stream()
.onEach(::handleViewEvents)
.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 {
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.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this, existingSession = true)
HomeActivity.newIntent(this, firstStartMainActivity = false, existingSession = true)
} else {
// The token is still invalid
navigator.softLogout(this)
@ -253,6 +323,10 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
null
}
}
startIntentAndFinish(intent)
}
private fun startIntentAndFinish(intent: Intent?) {
intent?.let { startActivity(it) }
finish()
}

View file

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

View file

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

View file

@ -604,7 +604,7 @@ class VectorCallActivity :
private fun returnToChat() {
val roomId = withState(callViewModel) { it.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
}
startActivity(intent)

View file

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

View file

@ -32,7 +32,8 @@ data class VerificationConclusionViewState(
enum class ConclusionState {
SUCCESS,
WARNING,
CANCELLED
CANCELLED,
INVALID_QR_CODE
}
class VerificationConclusionViewModel(initialState: VerificationConclusionViewState) :
@ -44,7 +45,9 @@ class VerificationConclusionViewModel(initialState: VerificationConclusionViewSt
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
return when (safeValueOf(args.cancelReason)) {
CancelCode.QrCodeInvalid,
CancelCode.QrCodeInvalid -> {
VerificationConclusionViewState(ConclusionState.INVALID_QR_CODE, args.isMe)
}
CancelCode.MismatchedUser,
CancelCode.MismatchedSas,
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.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.nightly.NightlyProxy
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -132,6 +133,7 @@ class HomeActivity :
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy
private val createSpaceResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
@ -238,7 +240,8 @@ class HomeActivity :
homeActivityViewModel.observeViewEvents {
when (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.StartRecoverySetupFlow -> handleStartRecoverySetup()
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
promptSecurityEvent(
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() {
popupAlertManager.postVectorAlert(
DefaultVectorAlert(
@ -533,6 +547,9 @@ class HomeActivity :
// Force remote backup state update to update the banner if needed
serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
// Check nightly
nightlyProxy.onHomeResumed()
}
override fun getMenuRes() = R.menu.home
@ -611,6 +628,7 @@ class HomeActivity :
companion object {
fun newIntent(
context: Context,
firstStartMainActivity: Boolean,
clearNotification: Boolean = false,
authenticationDescription: AuthenticationDescription? = null,
existingSession: Boolean = false,
@ -623,10 +641,16 @@ class HomeActivity :
inviteNotificationRoomId = inviteNotificationRoomId
)
return Intent(context, HomeActivity::class.java)
val intent = Intent(context, HomeActivity::class.java)
.apply {
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 {
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
object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -218,8 +217,7 @@ class HomeActivityViewModel @AssistedInject constructor(
private fun observeInitialSync() {
val session = activeSessionHolder.getSafeActiveSession() ?: return
session.syncService().getSyncRequestStateLive()
.asFlow()
session.syncService().getSyncRequestStateFlow()
.onEach { status ->
when (status) {
is SyncRequestState.Idle -> {
@ -364,14 +362,30 @@ class HomeActivityViewModel @AssistedInject constructor(
// If 4S is forced, force verification
_viewEvents.post(HomeActivityViewEvents.ForceVerification(true))
} else {
// New session
_viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// Always send request instead of waiting for an incoming as per recent EW changes
false
)
)
// we wan't to check if there is a way to actually verify this session,
// that means that there is another session to verify against, or
// secure backup is setup
val hasTargetDeviceToVerifyAgainst = session
.cryptoService()
.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.KeysBackupBanner
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.VectorCallActivity
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.RoomListParams
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.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale
@ -66,7 +68,8 @@ class HomeDetailFragment @Inject constructor(
private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences,
private val spaceStateHandler: SpaceStateHandler
private val spaceStateHandler: SpaceStateHandler,
private val vectorFeatures: VectorFeatures,
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate,
CurrentCallsView.Callback,
@ -352,8 +355,12 @@ class HomeDetailFragment @Inject constructor(
if (fragmentToShow == null) {
when (tab) {
is HomeTab.RoomList -> {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
if (vectorFeatures.isNewAppLayoutEnabled()) {
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 -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)

View file

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

View file

@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Live Location
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.platform.VectorBaseActivity
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.ViewRoom
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 ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT"
fun newIntent(context: Context, timelineArgs: TimelineArgs): Intent {
return Intent(context, RoomDetailActivity::class.java).apply {
fun newIntent(context: Context, timelineArgs: TimelineArgs, firstStartMainActivity: Boolean): Intent {
val intent = Intent(context, RoomDetailActivity::class.java).apply {
putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs)
}
return if (firstStartMainActivity) {
MainActivity.getIntentWithNextIntent(context, intent)
} else {
intent
}
}
// Shortcuts can't have intents with parcelables

View file

@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : 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.
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 isThreadTimeline() = rootThreadEventId != null

View file

@ -47,6 +47,11 @@ class StartCallActionsHandler(
}
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
when (roomSummary.joinedMembersCount) {
1 -> {

View file

@ -498,6 +498,7 @@ class TimelineFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
}
}
@ -859,6 +860,9 @@ class TimelineFragment @Inject constructor(
views.locationLiveStatusIndicator.stopButton.debouncedClicks {
timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing)
}
views.locationLiveStatusIndicator.debouncedClicks {
navigateToLocationLiveMap()
}
}
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
@ -1090,9 +1094,8 @@ class TimelineFragment @Inject constructor(
2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets
}
setOf(R.id.voice_call, R.id.video_call).forEach {
menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40
}
menu.findItem(R.id.video_call).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 widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
@ -1206,9 +1209,9 @@ class TimelineFragment @Inject constructor(
getRootThreadEventId()?.let {
val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it)
context?.let { con ->
val int = RoomDetailActivity.newIntent(con, newRoom)
int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
con.startActivity(int)
val intent = RoomDetailActivity.newIntent(con, newRoom, false)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
con.startActivity(intent)
}
}
}
@ -1257,7 +1260,7 @@ class TimelineFragment @Inject constructor(
val nonFormattedBody = when (messageContent) {
is MessageAudioContent -> getAudioContentBodyText(messageContent)
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()
}
var formattedBody: CharSequence? = null
@ -2653,6 +2656,15 @@ class TimelineFragment @Inject constructor(
.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() {
callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent(

View file

@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
@ -467,6 +466,13 @@ class TimelineViewModel @AssistedInject constructor(
}
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
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.invite -> state.canInvite
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
// 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
@ -1145,8 +1151,7 @@ class TimelineViewModel @AssistedInject constructor(
copy(syncState = syncState)
}
session.syncService().getSyncRequestStateLive()
.asFlow()
session.syncService().getSyncRequestStateFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncRequestState>()
.setOnEach {
copy(incrementalSyncRequestState = it)

View file

@ -102,7 +102,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes,
runningState: LiveLocationShareViewState.Running,
): MessageLiveLocationItem {
// TODO only render location if enabled in preferences: to be handled in a next PR
val width = timelineMediaSizeProvider.getMaxSize().first
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.graphics.drawable.ColorDrawable
import android.widget.ImageView
import androidx.core.content.ContextCompat
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.RoundedCorners
import im.vector.app.R
@ -50,8 +53,8 @@ class DefaultLiveLocationShareStatusItem : LiveLocationShareStatusItem {
height = mapHeight
}
GlideApp.with(mapImageView)
.load(R.drawable.bg_no_location_map)
.transform(mapCornerTransformation)
.load(ContextCompat.getDrawable(mapImageView.context, R.drawable.bg_no_location_map))
.transform(MultiTransformation(CenterCrop(), mapCornerTransformation))
.into(mapImageView)
}

View file

@ -42,7 +42,7 @@ abstract class MessageLiveLocationInactiveItem :
override fun getViewStubId() = 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)
}

View file

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

View file

@ -331,7 +331,7 @@ class RoomListSectionBuilder(
},
{ queryParams ->
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)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View file

@ -207,9 +207,18 @@ class RoomSummaryItemFactory @Inject constructor(
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
val userId = roomSummary.directUserId
val spaceName = roomSummary.flattenParents.lastOrNull()?.name
val directParent = joinParentNames(roomSummary)
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