mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 20:29:10 +03:00
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:
commit
e6addd1319
181 changed files with 3136 additions and 602 deletions
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
@ -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
|
||||
|
|
9
.github/workflows/triage-labelled.yml
vendored
9
.github/workflows/triage-labelled.yml
vendored
|
@ -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
1
changelog.d/2585.feature
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Enable improved login and register onboarding flows
|
1
changelog.d/5115.bugfix
Normal file
1
changelog.d/5115.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Stop using unstable names for withheld codes
|
1
changelog.d/6314.misc
Normal file
1
changelog.d/6314.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improves performance on search screen by replacing flattenParents with directParentName in RoomSummary
|
1
changelog.d/6341.bugfix
Normal file
1
changelog.d/6341.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fixed issues with reporting sync state events from different threads
|
1
changelog.d/6395.bugfix
Normal file
1
changelog.d/6395.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Display specific message when verification QR code is malformed
|
1
changelog.d/6466.bugfix
Normal file
1
changelog.d/6466.bugfix
Normal 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
1
changelog.d/6522.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve lock screen implementation with extra security measures
|
1
changelog.d/6548.feature
Normal file
1
changelog.d/6548.feature
Normal 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
1
changelog.d/6607.misc
Normal file
|
@ -0,0 +1 @@
|
|||
[Location sharing] - Small improvements of UI for live
|
1
changelog.d/6609.misc
Normal file
1
changelog.d/6609.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Live Location Sharing - Reset zoom level while focusing a user
|
1
changelog.d/6612.misc
Normal file
1
changelog.d/6612.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Fix a typo in the terms and conditions step during registration.
|
1
changelog.d/6616.feature
Normal file
1
changelog.d/6616.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Support element call widget
|
1
changelog.d/6620.feature
Normal file
1
changelog.d/6620.feature
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Test session feedback
|
1
changelog.d/6621.feature
Normal file
1
changelog.d/6621.feature
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Improved reset password error message
|
1
changelog.d/6622.feature
Normal file
1
changelog.d/6622.feature
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE - Allows the email address to be changed during the verification process
|
1
changelog.d/6625.misc
Normal file
1
changelog.d/6625.misc
Normal 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
1
changelog.d/6634.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Put EC permission shortcuts behind labs flag (PSG-630)
|
1
changelog.d/6635.misc
Normal file
1
changelog.d/6635.misc
Normal file
|
@ -0,0 +1 @@
|
|||
[Location Share] - Expanded map state when no more live location shares
|
|
@ -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",
|
||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
6
gradlew
vendored
|
@ -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
14
gradlew.bat
vendored
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("")) }
|
||||
}
|
||||
}
|
|
@ -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? = ""
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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() ?: "")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
75
vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt
Normal file
75
vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt
Normal 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()))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
|
@ -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") }
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
23
vector/src/fdroid/java/im/vector/app/nightly/NightlyProxy.kt
Normal file
23
vector/src/fdroid/java/im/vector/app/nightly/NightlyProxy.kt
Normal 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
|
||||
}
|
76
vector/src/gplay/java/im/vector/app/nightly/NightlyProxy.kt
Normal file
76
vector/src/gplay/java/im/vector/app/nightly/NightlyProxy.kt
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<*, *>
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
|
||||
// Live Location
|
||||
object StopLiveLocationSharing : RoomDetailAction()
|
||||
|
||||
object OpenElementCallWidget : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||
object StopChatEffects : RoomDetailViewEvents()
|
||||
object RoomReplacementStarted : RoomDetailViewEvents()
|
||||
object OpenElementCallWidget : RoomDetailViewEvents()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Add table
Reference in a new issue