diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 483291de1f..36fd225674 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -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
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index d2aa72308d..90f03779a6 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -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:
diff --git a/changelog.d/2585.feature b/changelog.d/2585.feature
new file mode 100644
index 0000000000..eedbac1e88
--- /dev/null
+++ b/changelog.d/2585.feature
@@ -0,0 +1 @@
+FTUE - Enable improved login and register onboarding flows
diff --git a/changelog.d/5115.bugfix b/changelog.d/5115.bugfix
new file mode 100644
index 0000000000..6b3ca4a7b4
--- /dev/null
+++ b/changelog.d/5115.bugfix
@@ -0,0 +1 @@
+Stop using unstable names for withheld codes
diff --git a/changelog.d/6314.misc b/changelog.d/6314.misc
new file mode 100644
index 0000000000..865d965d33
--- /dev/null
+++ b/changelog.d/6314.misc
@@ -0,0 +1 @@
+Improves performance on search screen by replacing flattenParents with directParentName in RoomSummary
diff --git a/changelog.d/6341.bugfix b/changelog.d/6341.bugfix
new file mode 100644
index 0000000000..6866d8c89d
--- /dev/null
+++ b/changelog.d/6341.bugfix
@@ -0,0 +1 @@
+Fixed issues with reporting sync state events from different threads
diff --git a/changelog.d/6395.bugfix b/changelog.d/6395.bugfix
new file mode 100644
index 0000000000..ebc22dc41a
--- /dev/null
+++ b/changelog.d/6395.bugfix
@@ -0,0 +1 @@
+Display specific message when verification QR code is malformed
diff --git a/changelog.d/6466.bugfix b/changelog.d/6466.bugfix
new file mode 100644
index 0000000000..31fef9f69d
--- /dev/null
+++ b/changelog.d/6466.bugfix
@@ -0,0 +1 @@
+When there is no way to verify a device (no 4S nor other device) propose to reset verification keys
diff --git a/changelog.d/6522.feature b/changelog.d/6522.feature
new file mode 100644
index 0000000000..fb5e535108
--- /dev/null
+++ b/changelog.d/6522.feature
@@ -0,0 +1 @@
+Improve lock screen implementation with extra security measures
diff --git a/changelog.d/6548.feature b/changelog.d/6548.feature
new file mode 100644
index 0000000000..8c40a37063
--- /dev/null
+++ b/changelog.d/6548.feature
@@ -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.
diff --git a/changelog.d/6607.misc b/changelog.d/6607.misc
new file mode 100644
index 0000000000..c56c3fca92
--- /dev/null
+++ b/changelog.d/6607.misc
@@ -0,0 +1 @@
+[Location sharing] - Small improvements of UI for live
diff --git a/changelog.d/6609.misc b/changelog.d/6609.misc
new file mode 100644
index 0000000000..bf1a9efe14
--- /dev/null
+++ b/changelog.d/6609.misc
@@ -0,0 +1 @@
+Live Location Sharing - Reset zoom level while focusing a user
diff --git a/changelog.d/6612.misc b/changelog.d/6612.misc
new file mode 100644
index 0000000000..ba80ff3e9d
--- /dev/null
+++ b/changelog.d/6612.misc
@@ -0,0 +1 @@
+Fix a typo in the terms and conditions step during registration.
diff --git a/changelog.d/6616.feature b/changelog.d/6616.feature
new file mode 100644
index 0000000000..d013771764
--- /dev/null
+++ b/changelog.d/6616.feature
@@ -0,0 +1 @@
+Support element call widget
diff --git a/changelog.d/6620.feature b/changelog.d/6620.feature
new file mode 100644
index 0000000000..ad192edd5c
--- /dev/null
+++ b/changelog.d/6620.feature
@@ -0,0 +1 @@
+FTUE - Test session feedback
diff --git a/changelog.d/6621.feature b/changelog.d/6621.feature
new file mode 100644
index 0000000000..b893c968b4
--- /dev/null
+++ b/changelog.d/6621.feature
@@ -0,0 +1 @@
+FTUE - Improved reset password error message
diff --git a/changelog.d/6622.feature b/changelog.d/6622.feature
new file mode 100644
index 0000000000..b3c8791ff0
--- /dev/null
+++ b/changelog.d/6622.feature
@@ -0,0 +1 @@
+FTUE - Allows the email address to be changed during the verification process
diff --git a/changelog.d/6625.misc b/changelog.d/6625.misc
new file mode 100644
index 0000000000..68a58c38fa
--- /dev/null
+++ b/changelog.d/6625.misc
@@ -0,0 +1 @@
+[Location sharing] - OnTap on the top live status bar, display the expanded map view
diff --git a/changelog.d/6634.bugfix b/changelog.d/6634.bugfix
new file mode 100644
index 0000000000..e135795ec2
--- /dev/null
+++ b/changelog.d/6634.bugfix
@@ -0,0 +1 @@
+Put EC permission shortcuts behind labs flag (PSG-630)
diff --git a/changelog.d/6635.misc b/changelog.d/6635.misc
new file mode 100644
index 0000000000..6546659d11
--- /dev/null
+++ b/changelog.d/6635.misc
@@ -0,0 +1 @@
+[Location Share] - Expanded map state when no more live location shares
diff --git a/dependencies.gradle b/dependencies.gradle
index 97b4ad2ea3..e8e39dc5f7 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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",
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 41d9927a4d..249e5832f0 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e1e0c8dc42..ef80eb5051 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/gradlew b/gradlew
index 1b6c787337..a69d9cb6c2 100755
--- a/gradlew
+++ b/gradlew
@@ -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.
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f938..53a6b238d4 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -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
diff --git a/library/ui-styles/src/main/res/values/stylable_location_live_ended_banner_view.xml b/library/ui-styles/src/main/res/values/stylable_location_live_ended_banner_view.xml
new file mode 100644
index 0000000000..81e377d39b
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_location_live_ended_banner_view.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index ae420a09b3..0aac4297e4 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -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)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index 80020665f8..18645fd6d9 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -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)
+ }
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index d3cc8fc8e4..6e198fb98c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -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
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
index bd2a1078b2..e701e0f3ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
@@ -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()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index fa3a9f6acd..8fdbba21c5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -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"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt
new file mode 100644
index 0000000000..c68a9e47f9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt
@@ -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)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index 90f3f323b2..ad8106c9c1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -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
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index 1ab23b7a11..ff4977491f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -164,9 +164,9 @@ data class RoomSummary(
*/
val spaceChildren: List? = 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 = emptyList(),
+ val directParentNames: List = emptyList(),
/**
* List of all the space parent Ids.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
index 5b2bf651af..71f7ab8494 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
@@ -60,9 +60,9 @@ interface SyncService {
fun getSyncStateLive(): LiveData
/**
- * Get the [SyncRequestState] as a LiveData.
+ * Get the [SyncRequestState] as a SharedFlow.
*/
- fun getSyncRequestStateLive(): LiveData
+ fun getSyncRequestStateFlow(): SharedFlow
/**
* This method returns a flow of SyncResponse. New value will be pushed through the sync thread.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
index ee098f9bf2..f02fe4f9de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
@@ -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 {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
index b7ea187ec5..900a2e237f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
@@ -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 whenAtLeast(version: Int, result: () -> T): T? {
+ return if (get() >= version) {
+ result()
+ } else null
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 850a4379ca..35c066dea8 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -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()
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
index 7f36224dae..729b4481e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
@@ -315,7 +315,7 @@ internal class IncomingKeyRequestManager @Inject constructor(
)
val params = SendToDeviceTask.Params(
- EventType.ROOM_KEY_WITHHELD,
+ EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap().apply {
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index ceaee582c7..96d97a41d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -365,7 +365,7 @@ internal class MXMegolmEncryption(
fromDevice = myDeviceId
)
val params = SendToDeviceTask.Params(
- EventType.ROOM_KEY_WITHHELD,
+ EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap().apply {
targets.forEach {
setObject(it.userId, it.deviceId, withHeldContent)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
index 854d148b76..b10e7501d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
@@ -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()?.code?.let {
RequestResult.Failure(it)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
index 690ac12268..5b1a4752f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
@@ -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
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 64e69bb3e9..b733aa6fc0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -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()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 735cfe411c..72b0f7a043 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt
new file mode 100644
index 0000000000..5b3c95b4a2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt
@@ -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("")) }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index 5fb4c3f3d8..471bec59af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -34,7 +34,8 @@ internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "",
var roomType: String? = null,
var parents: RealmList = RealmList(),
- var children: RealmList = RealmList()
+ var children: RealmList = RealmList(),
+ var directParentNames: RealmList = RealmList(),
) : RealmObject() {
private var displayName: String? = ""
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 6fd4f752a8..989bcaee44 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -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 {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index f9fa64ddfe..4fbc91e9ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -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() ?: "")
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index 80e27a1415..9d14ebffdd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -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 {
val liveRooms = monarchy.findAllManagedWithChanges {
roomSummariesQuery(it, queryParams)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index a721aeb935..7e064a84ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -223,6 +223,7 @@ internal class RoomSummaryUpdater @Inject constructor(
.sort(RoomSummaryEntityFields.ROOM_ID)
.findAll().map {
it.flattenParentIds = null
+ it.directParentNames.clear()
it to emptyList().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}")
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
index 37869b88f9..691dd7b20d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
@@ -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 {
- return syncRequestStateTracker.syncRequestState
- }
+ override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
index bcc5fcf9ab..03ce8cb3f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
@@ -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()
+ val syncRequestState = MutableSharedFlow()
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)
+ }
}
}
diff --git a/vector/build.gradle b/vector/build.gradle
index e4313770c4..0f65da4c2e 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -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
diff --git a/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt b/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt
new file mode 100644
index 0000000000..ba844e56b7
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt
@@ -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(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()))
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt
index b519d2f623..53c154ae30 100644
--- a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt
@@ -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(relaxed = true)
private val lockScreenKeyRepository = mockk(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 = 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(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
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt
index 68e1244791..6e02cc0262 100644
--- a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt
@@ -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
}
}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt
index 23eefe6577..924dbfee9e 100644
--- a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt
@@ -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() }
- }
}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt
similarity index 89%
rename from vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt
rename to vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt
index 297793c7a4..44c5db89c8 100644
--- a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt
@@ -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) }
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
index 3b54a8607b..b6fbfc23ab 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
@@ -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(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") }
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
index 350bbf8ba3..e72535c116 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
@@ -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(
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt
index bb09ee30f2..126cfafa18 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt
@@ -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 {
- pressBack()
- }
+ waitUntilViewVisible(withId(R.id.fons_scale_recycler))
+ pressBack()
}
}
diff --git a/vector/src/fdroid/java/im/vector/app/nightly/NightlyProxy.kt b/vector/src/fdroid/java/im/vector/app/nightly/NightlyProxy.kt
new file mode 100644
index 0000000000..eecf3a24f2
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/app/nightly/NightlyProxy.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.nightly
+
+import javax.inject.Inject
+
+class NightlyProxy @Inject constructor() {
+ fun onHomeResumed() = Unit
+}
diff --git a/vector/src/gplay/java/im/vector/app/nightly/NightlyProxy.kt b/vector/src/gplay/java/im/vector/app/nightly/NightlyProxy.kt
new file mode 100644
index 0000000000..7c6685f5ce
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/app/nightly/NightlyProxy.kt
@@ -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"
+ }
+}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 1c104f3bbf..fa89013707 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -308,7 +308,8 @@
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+ android:supportsPictureInPicture="true" />
@@ -380,6 +381,11 @@
android:exported="false"
android:foregroundServiceType="location" />
+
+
()
.map { session.spaceService().getRootSpaceSummaries().size }
.distinctUntilChanged()
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index a0a3b20e11..d3dfbf8c4f 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -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(
diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionSetter.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionSetter.kt
new file mode 100644
index 0000000000..09479a230f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionSetter.kt
@@ -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)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 675df69f23..08ba53a024 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -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
}
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index a3e08036ff..236622210f 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -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<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index 8fbd89a6c1..d9a08bd81a 100644
--- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
@@ -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() }
diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
index 53a5470ff7..be84dfeaba 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
@@ -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")
diff --git a/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt b/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt
new file mode 100644
index 0000000000..df84e24f90
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
index c2f6f2d778..61127e2c82 100644
--- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
@@ -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(), 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(), 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(), 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(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(), 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(), UnlockedActivity
null
}
}
+ startIntentAndFinish(intent)
+ }
+
+ private fun startIntentAndFinish(intent: Intent?) {
intent?.let { startActivity(it) }
finish()
}
diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
index b48fb62a3a..3a56f31b72 100644
--- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
+++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt
index 05358decc9..28929127e1 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt
@@ -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 ->
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
index 9d7ada9d63..f8a4c5eeca 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
index 9c5829eb8e..7bd0e393eb 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
@@ -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")
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
index aff2d807ac..8883ffb94e 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
@@ -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()
return when (safeValueOf(args.cancelReason)) {
- CancelCode.QrCodeInvalid,
+ CancelCode.QrCodeInvalid -> {
+ VerificationConclusionViewState(ConclusionState.INVALID_QR_CODE, args.isMe)
+ }
CancelCode.MismatchedUser,
CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment,
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 6e77975d46..389b4b7b27 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -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
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
index cb31a568e4..170550d5b4 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index b45e6fbcb0..eb24a1ba86 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -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(),
+ )
+ )
+ }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index 68e012f16e..d96b44b705 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -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(),
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)
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
index ede9872a9b..db0c85c419 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
@@ -198,8 +198,7 @@ class HomeDetailViewModel @AssistedInject constructor(
copy(syncState = syncState)
}
- session.syncService().getSyncRequestStateLive()
- .asFlow()
+ session.syncService().getSyncRequestStateFlow()
.filterIsInstance()
.setOnEach {
copy(incrementalSyncRequestState = it)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 64670c73ac..c1e3b58a80 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Live Location
object StopLiveLocationSharing : RoomDetailAction()
+
+ object OpenElementCallWidget : RoomDetailAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
index f1e06dd5ef..a58eed42e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
index dcfee2d919..3af849e965 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
@@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : RoomDetailViewEvents()
object RoomReplacementStarted : RoomDetailViewEvents()
+ object OpenElementCallWidget : RoomDetailViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
index 8500d1ed96..7aa7d5a877 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
index ba691de5d2..8d2d086275 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
@@ -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 -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 31c1004ef9..4c924b32aa 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -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(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 2e313f04ae..848bd3aed4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -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()
.setOnEach {
copy(incrementalSyncRequestState = it)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
index a4c906d97b..493602a291 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt
index c421efda12..9a729caa31 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt
@@ -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)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt
index bc6e96b0ee..fae8091f06 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt
@@ -42,7 +42,7 @@ abstract class MessageLiveLocationInactiveItem :
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
- val bannerImageView by bind(R.id.locationLiveInactiveBanner)
+ val bannerImageView by bind(R.id.locationLiveEndedBannerBackground)
val noLocationMapImageView by bind(R.id.locationLiveInactiveMap)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt
index 84080eaad9..b35b4dff8b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt
@@ -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(R.id.locationLiveMessageBanner)
+ val locationLiveRunningBanner by bind(R.id.locationLiveRunningBanner)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt
index 8c422e60b4..686302ca36 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt
@@ -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()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index f50cec5149..85879e6807 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -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)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt
new file mode 100644
index 0000000000..04c8524b50
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt
new file mode 100644
index 0000000000..f0eb027785
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt
@@ -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(),
+ 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) {
+ 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
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewEvents.kt
new file mode 100644
index 0000000000..a80ae9fa93
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewEvents.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt
new file mode 100644
index 0000000000..3226ed24f2
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt
@@ -0,0 +1,206 @@
+/*
+ * 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 androidx.paging.PagedList
+import arrow.core.toOption
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.AppStateHandler
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.StateView
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.home.room.list.RoomListViewModel
+import im.vector.app.features.settings.VectorPreferences
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.query.SpaceFilter
+import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.api.session.room.state.isPublic
+
+class HomeRoomListViewModel @AssistedInject constructor(
+ @Assisted initialState: HomeRoomListViewState,
+ private val session: Session,
+ private val appStateHandler: AppStateHandler,
+ private val vectorPreferences: VectorPreferences,
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: HomeRoomListViewState): HomeRoomListViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ private val pagedListConfig = PagedList.Config.Builder()
+ .setPageSize(10)
+ .setInitialLoadSizeHint(20)
+ .setEnablePlaceholders(true)
+ .setPrefetchDistance(10)
+ .build()
+
+ private val _sections = MutableSharedFlow>(replay = 1)
+ val sections = _sections.asSharedFlow()
+
+ init {
+ configureSections()
+ }
+
+ private fun configureSections() {
+ val newSections = mutableSetOf()
+
+ newSections.add(getAllRoomsSection())
+
+ viewModelScope.launch {
+ _sections.emit(newSections)
+ }
+
+ setState {
+ copy(state = StateView.State.Content)
+ }
+ }
+
+ private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData {
+ val builder = RoomSummaryQueryParams.Builder().also {
+ it.memberships = listOf(Membership.JOIN)
+ }
+
+ val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive(
+ builder.build(),
+ pagedListConfig
+ )
+
+ appStateHandler.selectedSpaceFlow
+ .distinctUntilChanged()
+ .onStart {
+ emit(appStateHandler.getCurrentSpace().toOption())
+ }
+ .onEach { selectedSpaceOption ->
+ val selectedSpace = selectedSpaceOption.orNull()
+ val strategy = if (!vectorPreferences.prefSpacesShowAllRoomInHome()) {
+ RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
+ } else {
+ RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
+ }
+ filteredPagedRoomSummariesLive.queryParams = filteredPagedRoomSummariesLive.queryParams.copy(
+ spaceFilter = getSpaceFilter(selectedSpaceId = selectedSpace?.roomId, strategy)
+ )
+ }.launchIn(viewModelScope)
+
+ return HomeRoomSection.RoomSummaryData(
+ list = filteredPagedRoomSummariesLive.livePagedList
+ )
+ }
+
+ private fun getSpaceFilter(selectedSpaceId: String?, strategy: RoomListViewModel.SpaceFilterStrategy): SpaceFilter? {
+ return when (strategy) {
+ RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
+ selectedSpaceId?.toActiveSpaceOrOrphanRooms()
+ }
+ RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
+ selectedSpaceId?.let { SpaceFilter.ActiveSpace(it) }
+ }
+ RoomListViewModel.SpaceFilterStrategy.NONE -> null
+ }
+ }
+
+ override fun handle(action: HomeRoomListAction) {
+ when (action) {
+ is HomeRoomListAction.SelectRoom -> handleSelectRoom(action)
+ is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action)
+ is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
+ is HomeRoomListAction.ToggleTag -> handleToggleTag(action)
+ }
+ }
+
+ fun isPublicRoom(roomId: String): Boolean {
+ return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
+ }
+
+ private fun handleSelectRoom(action: HomeRoomListAction.SelectRoom) = withState {
+ _viewEvents.post(HomeRoomListViewEvents.SelectRoom(action.roomSummary, false))
+ }
+
+ private fun handleLeaveRoom(action: HomeRoomListAction.LeaveRoom) {
+ _viewEvents.post(HomeRoomListViewEvents.Loading(null))
+ viewModelScope.launch {
+ val value = runCatching { session.roomService().leaveRoom(action.roomId) }
+ .fold({ HomeRoomListViewEvents.Done }, { HomeRoomListViewEvents.Failure(it) })
+ _viewEvents.post(value)
+ }
+ }
+
+ private fun handleChangeNotificationMode(action: HomeRoomListAction.ChangeRoomNotificationState) {
+ val room = session.getRoom(action.roomId)
+ if (room != null) {
+ viewModelScope.launch {
+ try {
+ room.roomPushRuleService().setRoomNotificationState(action.notificationState)
+ } catch (failure: Exception) {
+ _viewEvents.post(HomeRoomListViewEvents.Failure(failure))
+ }
+ }
+ }
+ }
+
+ private fun handleToggleTag(action: HomeRoomListAction.ToggleTag) {
+ session.getRoom(action.roomId)?.let { room ->
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ if (room.roomSummary()?.hasTag(action.tag) == false) {
+ // Favorite and low priority tags are exclusive, so maybe delete the other tag first
+ action.tag.otherTag()
+ ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }
+ ?.let { tagToRemove ->
+ room.tagsService().deleteTag(tagToRemove)
+ }
+
+ // Set the tag. We do not handle the order for the moment
+ room.tagsService().addTag(action.tag, 0.5)
+ } else {
+ room.tagsService().deleteTag(action.tag)
+ }
+ } catch (failure: Throwable) {
+ _viewEvents.post(HomeRoomListViewEvents.Failure(failure))
+ }
+ }
+ }
+ }
+
+ private fun String.otherTag(): String? {
+ return when (this) {
+ RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY
+ RoomTag.ROOM_TAG_LOW_PRIORITY -> RoomTag.ROOM_TAG_FAVOURITE
+ else -> null
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt
new file mode 100644
index 0000000000..bfcaea22e9
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 com.airbnb.mvrx.MavericksState
+import im.vector.app.core.platform.StateView
+
+data class HomeRoomListViewState(
+ val state: StateView.State = StateView.State.Loading
+) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt
new file mode 100644
index 0000000000..7bfd0a769e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.list.home
+
+import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+sealed class HomeRoomSection {
+ data class RoomSummaryData(
+ val list: LiveData>
+ ) : HomeRoomSection()
+}
diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt
index 6de73cb20f..0bdec53f60 100644
--- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt
@@ -18,18 +18,23 @@ package im.vector.app.features.link
import android.content.Intent
import android.net.Uri
+import android.os.Bundle
import androidx.lifecycle.lifecycleScope
+import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityProgressBinding
+import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.permalink.PermalinkHandler
+import im.vector.app.features.start.StartAppViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import timber.log.Timber
@@ -45,12 +50,33 @@ class LinkHandlerActivity : VectorBaseActivity() {
@Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var permalinkHandler: PermalinkHandler
+ private val startAppViewModel: StartAppViewModel by viewModel()
+
override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater)
override fun initUiAndData() {
handleIntent()
}
+ private val launcher = registerStartForActivityResult {
+ if (it.resultCode == RESULT_OK) {
+ handleIntent()
+ } else {
+ // User has pressed back on the MainActivity, so finish also this one.
+ finish()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (startAppViewModel.shouldStartApp()) {
+ launcher.launch(MainActivity.getIntentToInitSession(this))
+ } else {
+ handleIntent()
+ }
+ }
+
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent()
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt
index dd18658059..635c0bf87d 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt
@@ -122,7 +122,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
?.locationSharingService()
?.startLiveLocationShare(
timeoutMillis = roomArgs.durationMillis,
- description = getString(R.string.sent_live_location)
+ description = getString(R.string.live_location_description)
)
updateLiveResult
diff --git a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt
index cbfdf1dfda..8e917c665a 100644
--- a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt
@@ -28,10 +28,12 @@ fun MapboxMap?.zoomToLocation(locationData: LocationData, preserveCurrentZoomLev
} else {
INITIAL_MAP_ZOOM_IN_PREVIEW
}
- this?.cameraPosition = CameraPosition.Builder()
- .target(LatLng(locationData.latitude, locationData.longitude))
- .zoom(zoomLevel)
- .build()
+ this?.easeCamera {
+ CameraPosition.Builder()
+ .target(LatLng(locationData.latitude, locationData.longitude))
+ .zoom(zoomLevel)
+ .build()
+ }
}
fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) {
diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
index 1f9cb44c91..491386ba64 100644
--- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
@@ -22,6 +22,7 @@ import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageView
import androidx.core.content.ContextCompat
+import androidx.core.content.res.use
import androidx.core.view.marginBottom
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
@@ -60,17 +61,13 @@ class MapTilerMapView @JvmOverloads constructor(
private var dimensionConverter: DimensionConverter? = null
init {
- context.theme.obtainStyledAttributes(
+ context.obtainStyledAttributes(
attrs,
R.styleable.MapTilerMapView,
0,
0
- ).run {
- try {
- setLocateButtonVisibility(this)
- } finally {
- recycle()
- }
+ ).use {
+ setLocateButtonVisibility(it)
}
dimensionConverter = DimensionConverter(resources)
}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveEndedBannerView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveEndedBannerView.kt
new file mode 100644
index 0000000000..82fa17a625
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveEndedBannerView.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.location.live
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.res.use
+import androidx.core.view.updateLayoutParams
+import im.vector.app.R
+import im.vector.app.databinding.ViewLocationLiveEndedBannerBinding
+
+private const val BACKGROUND_ALPHA = 0.75f
+
+class LocationLiveEndedBannerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val binding = ViewLocationLiveEndedBannerBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+
+ init {
+ context.obtainStyledAttributes(
+ attrs,
+ R.styleable.LocationLiveEndedBannerView,
+ 0,
+ 0
+ ).use {
+ setBackgroundAlpha(it)
+ setIconMarginStart(it)
+ }
+ }
+
+ private fun setBackgroundAlpha(typedArray: TypedArray) {
+ val withAlpha = typedArray.getBoolean(R.styleable.LocationLiveEndedBannerView_locLiveEndedBkgWithAlpha, false)
+ binding.locationLiveEndedBannerBackground.alpha = if (withAlpha) BACKGROUND_ALPHA else 1f
+ }
+
+ private fun setIconMarginStart(typedArray: TypedArray) {
+ val margin = typedArray.getDimensionPixelOffset(R.styleable.LocationLiveEndedBannerView_locLiveEndedIconMarginStart, 0)
+ binding.locationLiveEndedBannerIcon.updateLayoutParams {
+ marginStart = margin
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveRunningBannerView.kt
similarity index 87%
rename from vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt
rename to vector/src/main/java/im/vector/app/features/location/live/LocationLiveRunningBannerView.kt
index 51c7caed3a..4ca8475da1 100644
--- a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveRunningBannerView.kt
@@ -31,34 +31,34 @@ import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.TextUtils
-import im.vector.app.databinding.ViewLocationLiveMessageBannerBinding
+import im.vector.app.databinding.ViewLocationLiveRunningBannerBinding
import im.vector.app.features.themes.ThemeUtils
import org.threeten.bp.Duration
private const val REMAINING_TIME_COUNTER_INTERVAL_IN_MS = 1000L
-class LocationLiveMessageBannerView @JvmOverloads constructor(
+class LocationLiveRunningBannerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
- private val binding = ViewLocationLiveMessageBannerBinding.inflate(
+ private val binding = ViewLocationLiveRunningBannerBinding.inflate(
LayoutInflater.from(context),
this
)
val stopButton: Button
- get() = binding.locationLiveMessageBannerStop
+ get() = binding.locationLiveRunningBannerStop
private val background: ImageView
- get() = binding.locationLiveMessageBannerBackground
+ get() = binding.locationLiveRunningBannerBackground
private val title: TextView
- get() = binding.locationLiveMessageBannerTitle
+ get() = binding.locationLiveRunningBannerTitle
private val subTitle: TextView
- get() = binding.locationLiveMessageBannerSubTitle
+ get() = binding.locationLiveRunningBannerSubTitle
private var countDownTimer: CountDownTimer? = null
@@ -70,7 +70,7 @@ class LocationLiveMessageBannerView @JvmOverloads constructor(
GlideApp.with(context)
.load(ColorDrawable(ThemeUtils.getColor(context, android.R.attr.colorBackground)))
- .placeholder(binding.locationLiveMessageBannerBackground.drawable)
+ .placeholder(binding.locationLiveRunningBannerBackground.drawable)
.transform(GranularRoundedCorners(0f, 0f, viewState.bottomEndCornerRadiusInDp, viewState.bottomStartCornerRadiusInDp))
.into(background)
}
@@ -109,14 +109,14 @@ class LocationLiveMessageBannerView @JvmOverloads constructor(
if (viewState.isStopButtonCenteredVertically) {
constraintSet.connect(
- R.id.locationLiveMessageBannerStop,
+ R.id.locationLiveRunningBannerStop,
ConstraintSet.BOTTOM,
- R.id.locationLiveMessageBannerBackground,
+ R.id.locationLiveRunningBannerBackground,
ConstraintSet.BOTTOM,
0
)
} else {
- constraintSet.clear(R.id.locationLiveMessageBannerStop, ConstraintSet.BOTTOM)
+ constraintSet.clear(R.id.locationLiveRunningBannerStop, ConstraintSet.BOTTOM)
}
constraintSet.applyTo(parentLayout)
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
index 1d6afa9cda..3aacd70f0e 100644
--- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt
@@ -22,6 +22,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.drawable.toBitmap
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@@ -45,6 +47,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.openLocation
import im.vector.app.databinding.FragmentLocationLiveMapViewBinding
+import im.vector.app.features.location.LocationData
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.zoomToBounds
import im.vector.app.features.location.zoomToLocation
@@ -56,7 +59,6 @@ import javax.inject.Inject
/**
* Screen showing a map with all the current users sharing their live location in a room.
*/
-
@AndroidEntryPoint
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment() {
@@ -109,13 +111,6 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment
- val bottomSheetHeight = BottomSheetBehavior.from(views.bottomSheet).peekHeight
- mapboxMap.uiSettings.apply {
- // Place copyright above the user list bottom sheet
- setLogoMargins(dimensionConverter.dpToPx(8), 0, 0, bottomSheetHeight + dimensionConverter.dpToPx(8))
- setAttributionMargins(dimensionConverter.dpToPx(96), 0, 0, bottomSheetHeight + dimensionConverter.dpToPx(8))
- }
-
lifecycleScope.launch {
mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style ->
mapStyle = style
@@ -137,11 +132,9 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment) {
+ if (userLocations.isEmpty()) {
+ showEndedLiveBanner()
+ } else {
+ showUserList(userLocations)
+ }
+ }
+
+ private fun showEndedLiveBanner() {
+ views.bottomSheet.isGone = true
+ views.liveLocationMapFragmentEndedBanner.isVisible = true
+ updateCopyrightMargin(bottomOffset = views.liveLocationMapFragmentEndedBanner.height)
+ }
+
+ private fun showUserList(userLocations: List) {
+ val bottomSheetHeight = BottomSheetBehavior.from(views.bottomSheet).peekHeight
+ updateCopyrightMargin(bottomOffset = bottomSheetHeight)
+ views.bottomSheet.isVisible = true
+ views.liveLocationMapFragmentEndedBanner.isGone = true
bottomSheetController.setData(userLocations)
}
+ private fun updateCopyrightMargin(bottomOffset: Int) {
+ getOrCreateSupportMapFragment().getMapAsync { mapboxMap ->
+ mapboxMap.uiSettings.apply {
+ // Place copyright above the user list bottom sheet
+ setLogoMargins(
+ dimensionConverter.dpToPx(COPYRIGHT_MARGIN_DP),
+ 0,
+ 0,
+ bottomOffset + dimensionConverter.dpToPx(COPYRIGHT_MARGIN_DP)
+ )
+ setAttributionMargins(
+ dimensionConverter.dpToPx(COPYRIGHT_ATTRIBUTION_MARGIN_DP),
+ 0,
+ 0,
+ bottomOffset + dimensionConverter.dpToPx(COPYRIGHT_MARGIN_DP)
+ )
+ }
+ }
+ }
+
private fun updateMap(userLiveLocations: List) {
symbolManager?.let { sManager ->
val latLngBoundsBuilder = LatLngBounds.Builder()
@@ -273,11 +304,13 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment
- mapboxMap?.get()?.zoomToLocation(locationData, preserveCurrentZoomLevel = true)
+ mapboxMap?.get()?.zoomToLocation(locationData, preserveCurrentZoomLevel = false)
}
}
companion object {
private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map"
+ private const val COPYRIGHT_MARGIN_DP = 8
+ private const val COPYRIGHT_ATTRIBUTION_MARGIN_DP = 96
}
}
diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt
index d11ff00261..a4bc694aaf 100644
--- a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt
@@ -24,6 +24,7 @@ import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
+import androidx.core.content.res.use
import androidx.core.view.setPadding
import im.vector.app.R
import im.vector.app.core.extensions.tintBackground
@@ -45,18 +46,14 @@ class LocationSharingOptionView @JvmOverloads constructor(
)
init {
- context.theme.obtainStyledAttributes(
+ context.obtainStyledAttributes(
attrs,
R.styleable.LocationSharingOptionView,
0,
0
- ).run {
- try {
- setIcon(this)
- setTitle(this)
- } finally {
- recycle()
- }
+ ).use {
+ setIcon(it)
+ setTitle(it)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
index 4cbebd67a3..763d1eed38 100644
--- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
@@ -221,7 +221,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA
analyticsScreenName = MobileScreen.ScreenName.Register
}
val authDescription = inferAuthDescription(loginViewState)
- val intent = HomeActivity.newIntent(this, authenticationDescription = authDescription)
+ val intent = HomeActivity.newIntent(this, firstStartMainActivity = false, authenticationDescription = authDescription)
startActivity(intent)
finish()
return
diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
index 877aea4ba3..67bc9a78e7 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
@@ -173,7 +173,7 @@ class DefaultNavigator @Inject constructor(
}
val args = TimelineArgs(roomId = roomId, eventId = eventId, isInviteAlreadyAccepted = isInviteAlreadyAccepted)
- val intent = RoomDetailActivity.newIntent(context, args)
+ val intent = RoomDetailActivity.newIntent(context, args, false)
startActivity(context, intent, buildTask)
}
@@ -203,7 +203,7 @@ class DefaultNavigator @Inject constructor(
eventId = null,
openShareSpaceForId = spaceId.takeIf { postSwitchSpaceAction.showShareSheet }
)
- val intent = RoomDetailActivity.newIntent(context, args)
+ val intent = RoomDetailActivity.newIntent(context, args, false)
startActivity(context, intent, false)
}
}
@@ -290,7 +290,7 @@ class DefaultNavigator @Inject constructor(
override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) {
val args = TimelineArgs(roomId, null, sharedData)
- val intent = RoomDetailActivity.newIntent(activity, args)
+ val intent = RoomDetailActivity.newIntent(activity, args, false)
activity.startActivity(intent)
activity.finish()
}
@@ -465,6 +465,9 @@ class DefaultNavigator @Inject constructor(
val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true
context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo))
}
+ } else if (widget.type is WidgetType.ElementCall) {
+ val widgetArgs = widgetArgsBuilder.buildElementCallWidgetArgs(roomId, widget)
+ context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
} else {
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
index 71c8167788..2948565d58 100755
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
@@ -53,6 +53,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.core.services.CallAndroidService
import im.vector.app.core.time.Clock
import im.vector.app.core.utils.startNotificationChannelSettingsIntent
+import im.vector.app.features.MainActivity
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.service.CallHeadsUpActionReceiver
import im.vector.app.features.call.webrtc.WebRtcCall
@@ -239,9 +240,10 @@ class NotificationUtils @Inject constructor(
@SuppressLint("NewApi")
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
// build the pending intent go to the home screen if this is clicked.
- val i = HomeActivity.newIntent(context)
+ val i = HomeActivity.newIntent(context, firstStartMainActivity = false)
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- val pi = PendingIntent.getActivity(context, 0, i, PendingIntentCompat.FLAG_IMMUTABLE)
+ val mainIntent = MainActivity.getIntentWithNextIntent(context, i)
+ val pi = PendingIntent.getActivity(context, 0, mainIntent, PendingIntentCompat.FLAG_IMMUTABLE)
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
@@ -344,7 +346,7 @@ class NotificationUtils @Inject constructor(
)
val answerCallPendingIntent = TaskStackBuilder.create(context)
- .addNextIntentWithParentStack(HomeActivity.newIntent(context))
+ .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
.addNextIntent(
VectorCallActivity.newIntent(
context = context,
@@ -468,7 +470,7 @@ class NotificationUtils @Inject constructor(
)
val contentPendingIntent = TaskStackBuilder.create(context)
- .addNextIntentWithParentStack(HomeActivity.newIntent(context))
+ .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
.addNextIntent(VectorCallActivity.newIntent(context, call, null))
.getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
@@ -530,8 +532,8 @@ class NotificationUtils @Inject constructor(
.setCategory(NotificationCompat.CATEGORY_CALL)
val contentPendingIntent = TaskStackBuilder.create(context)
- .addNextIntentWithParentStack(HomeActivity.newIntent(context))
- .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId)))
+ .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
+ .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId), true))
.getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
builder.setContentIntent(contentPendingIntent)
@@ -566,6 +568,19 @@ class NotificationUtils @Inject constructor(
.build()
}
+ /**
+ * Creates a notification that indicates the application is initializing.
+ */
+ fun buildStartAppNotification(): Notification {
+ return NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(stringProvider.getString(R.string.updating_your_data))
+ .setSmallIcon(R.drawable.sync)
+ .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+ }
+
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
.setGroup(stringProvider.getString(R.string.app_name))
@@ -765,7 +780,11 @@ class NotificationUtils @Inject constructor(
joinIntentPendingIntent
)
- val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId)
+ val contentIntent = HomeActivity.newIntent(
+ context,
+ firstStartMainActivity = true,
+ inviteNotificationRoomId = inviteNotifiableEvent.roomId
+ )
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
@@ -806,7 +825,7 @@ class NotificationUtils @Inject constructor(
.setColor(accentColor)
.setAutoCancel(true)
.apply {
- val contentIntent = HomeActivity.newIntent(context)
+ val contentIntent = HomeActivity.newIntent(context, firstStartMainActivity = true)
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId)
@@ -828,14 +847,14 @@ class NotificationUtils @Inject constructor(
}
private fun buildOpenRoomIntent(roomId: String): PendingIntent? {
- val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true))
+ val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true), true)
roomIntentTap.action = TAP_TO_VIEW_ACTION
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
roomIntentTap.data = createIgnoredUri("openRoom?$roomId")
// Recreate the back stack
return TaskStackBuilder.create(context)
- .addNextIntentWithParentStack(HomeActivity.newIntent(context))
+ .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
.addNextIntent(roomIntentTap)
.getPendingIntent(
clock.epochMillis().toInt(),
@@ -844,13 +863,14 @@ class NotificationUtils @Inject constructor(
}
private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
- val intent = HomeActivity.newIntent(context, clearNotification = true)
+ val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.data = createIgnoredUri("tapSummary")
+ val mainIntent = MainActivity.getIntentWithNextIntent(context, intent)
return PendingIntent.getActivity(
context,
Random.nextInt(1000),
- intent,
+ mainIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
index 0d7c83e360..7def6d62f0 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
@@ -302,7 +302,8 @@ class Login2Variant(
private fun terminate() {
val intent = HomeActivity.newIntent(
- activity
+ activity,
+ firstStartMainActivity = false,
)
activity.startActivity(intent)
activity.finish()
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
index ea6981a2b5..bbbf13fba9 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
@@ -53,7 +53,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OnResetPasswordBreakerConfirmed : OnboardingViewEvents()
object OnResetPasswordComplete : OnboardingViewEvents()
- data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents()
+ data class OnSendEmailSuccess(val email: String, val isRestoredSession: Boolean) : OnboardingViewEvents()
data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents()
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : OnboardingViewEvents()
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 52c32d88e4..6cadb4308a 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -348,7 +348,10 @@ class OnboardingViewModel @AssistedInject constructor(
overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration)
}
RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback)
- is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email))
+ is RegistrationActionHandler.Result.SendEmailSuccess -> {
+ _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email, isRestoredSession = false))
+ setState { copy(registrationState = registrationState.copy(email = it.email)) }
+ }
is RegistrationActionHandler.Result.SendMsisdnSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendMsisdnSuccess(it.msisdn.msisdn))
is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause))
RegistrationActionHandler.Result.MissingNextStage -> {
@@ -412,8 +415,8 @@ class OnboardingViewModel @AssistedInject constructor(
authenticationService.cancelPendingLoginOrRegistration()
setState {
copy(
- isLoading = false,
- registrationState = RegistrationState(),
+ isLoading = false,
+ registrationState = RegistrationState(),
)
}
}
@@ -486,7 +489,7 @@ class OnboardingViewModel @AssistedInject constructor(
try {
if (registrationWizard.isRegistrationStarted()) {
currentThreePid?.let {
- handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
+ handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it, isRestoredSession = true)))
}
}
} catch (e: Throwable) {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
index fe2134618d..99678ea5c1 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
@@ -101,6 +101,7 @@ data class SelectedAuthenticationState(
@Parcelize
data class RegistrationState(
+ val email: String? = null,
val isUserNameAvailable: Boolean = false,
val selectedMatrixId: String? = null,
) : Parcelable
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
index 7766523de9..072e94bc30 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
@@ -46,6 +46,7 @@ abstract class AbstractFtueAuthFragment : VectorBaseFragment : VectorBaseFragment {
+ displayCancelDialog && viewModel.isRegistrationStarted && backIsHardExit() -> {
// Ask for confirmation before cancelling the registration
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.login_signup_cancel_confirmation_title)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt
index 639045b5c0..c69706a17b 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt
@@ -75,7 +75,11 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
setupSubmitButton()
views.createAccountRoot.realignPercentagesToParent()
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
- views.createAccountPasswordInput.setOnImeDoneListener { submit() }
+ views.createAccountPasswordInput.setOnImeDoneListener {
+ if (canSubmit(views.createAccountInput.content(), views.createAccountPasswordInput.content())) {
+ submit()
+ }
+ }
views.createAccountInput.onTextChange(viewLifecycleOwner) {
viewModel.handle(OnboardingAction.ResetSelectedRegistrationUserName)
@@ -87,15 +91,19 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
}
}
+ private fun canSubmit(account: CharSequence, password: CharSequence): Boolean {
+ val accountIsValid = account.isNotEmpty()
+ val passwordIsValid = password.length >= MINIMUM_PASSWORD_LENGTH
+ return accountIsValid && passwordIsValid
+ }
+
private fun setupSubmitButton() {
views.createAccountSubmit.setOnClickListener { submit() }
views.createAccountInput.clearErrorOnChange(viewLifecycleOwner)
views.createAccountPasswordInput.clearErrorOnChange(viewLifecycleOwner)
combine(views.createAccountInput.editText().textChanges(), views.createAccountPasswordInput.editText().textChanges()) { account, password ->
- val accountIsValid = account.isNotEmpty()
- val passwordIsValid = password.length >= MINIMUM_PASSWORD_LENGTH
- views.createAccountSubmit.isEnabled = accountIsValid && passwordIsValid
+ views.createAccountSubmit.isEnabled = canSubmit(account, password)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
index bc44a7dbdb..749aac2898 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
@@ -20,18 +20,20 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.inputmethod.EditorInfo
import im.vector.app.R
+import im.vector.app.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.clearErrorOnChange
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.realignPercentagesToParent
+import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
import im.vector.app.features.onboarding.OnboardingAction
+import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
@@ -53,19 +55,19 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
views.chooseServerToolbar.setNavigationOnClickListener {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnBack))
}
- views.chooseServerInput.editText?.setOnEditorActionListener { _, actionId, _ ->
- when (actionId) {
- EditorInfo.IME_ACTION_DONE -> {
- updateServerUrl()
- }
+ views.chooseServerInput.associateContentStateWith(button = views.chooseServerSubmit, enabledPredicate = { canSubmit(it) })
+ views.chooseServerInput.setOnImeDoneListener {
+ if (canSubmit(views.chooseServerInput.content())) {
+ updateServerUrl()
}
- false
}
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner)
}
+ private fun canSubmit(url: String) = url.isNotEmpty()
+
private fun updateServerUrl() {
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(views.chooseServerInput.content().ensureProtocol().ensureTrailingSlash()))
}
@@ -75,6 +77,14 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt
}
override fun updateWithState(state: OnboardingViewState) {
+ views.chooseServerHeaderSubtitle.setText(
+ when (state.onboardingFlow) {
+ OnboardingFlow.SignIn -> R.string.ftue_auth_choose_server_sign_in_subtitle
+ OnboardingFlow.SignUp -> R.string.ftue_auth_choose_server_subtitle
+ else -> throw IllegalStateException("Invalid flow state")
+ }
+ )
+
if (views.chooseServerInput.content().isEmpty()) {
val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure()
views.chooseServerInput.editText().setText(userUrlInput)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt
index 61da7e0d18..5de8fce82f 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt
@@ -25,6 +25,8 @@ import im.vector.app.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.autofillEmail
import im.vector.app.core.extensions.clearErrorOnChange
import im.vector.app.core.extensions.content
+import im.vector.app.core.extensions.editText
+import im.vector.app.core.extensions.hasContent
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
@@ -61,6 +63,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
override fun updateWithState(state: OnboardingViewState) {
views.emailEntryHeaderSubtitle.text = getString(R.string.ftue_auth_email_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl())
+
+ if (!views.emailEntryInput.hasContent()) {
+ views.emailEntryInput.editText().setText(state.registrationState.email)
+ }
}
override fun onError(throwable: Throwable) {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt
index 6282fded61..61826352bf 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt
@@ -31,6 +31,7 @@ import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.databinding.FragmentFtueResetPasswordInputBinding
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState
+import org.matrix.android.sdk.api.failure.isMissingEmailVerification
@AndroidEntryPoint
class FtueAuthResetPasswordEntryFragment : AbstractFtueAuthFragment() {
@@ -61,7 +62,12 @@ class FtueAuthResetPasswordEntryFragment : AbstractFtueAuthFragment super.onError(throwable)
+ else -> {
+ views.newPasswordInput.error = errorFormatter.toHumanReadable(throwable)
+ }
+ }
}
override fun updateWithState(state: OnboardingViewState) {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index bb8c523b5f..150ab74ec2 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -196,7 +196,7 @@ class FtueAuthVariant(
activity.popBackstack()
}
is OnboardingViewEvents.OnSendEmailSuccess -> {
- openWaitForEmailVerification(viewEvents.email)
+ openWaitForEmailVerification(viewEvents.email, viewEvents.isRestoredSession)
}
is OnboardingViewEvents.OnSendMsisdnSuccess -> {
openMsisdnConfirmation(viewEvents.msisdn)
@@ -413,17 +413,19 @@ class FtueAuthVariant(
}
}
- private fun openWaitForEmailVerification(email: String) {
- supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ private fun openWaitForEmailVerification(email: String, isRestoredSession: Boolean) {
when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
FtueAuthWaitForEmailFragment::class.java,
- FtueAuthWaitForEmailFragmentArgument(email),
- )
- else -> addRegistrationStageFragmentToBackstack(
- FtueAuthLegacyWaitForEmailFragment::class.java,
- FtueAuthWaitForEmailFragmentArgument(email),
+ FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
)
+ else -> {
+ supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ addRegistrationStageFragmentToBackstack(
+ FtueAuthLegacyWaitForEmailFragment::class.java,
+ FtueAuthWaitForEmailFragmentArgument(email, isRestoredSession),
+ )
+ }
}
}
@@ -482,7 +484,11 @@ class FtueAuthVariant(
private fun navigateToHome() {
withState(onboardingViewModel) {
- val intent = HomeActivity.newIntent(activity, authenticationDescription = it.selectedAuthenticationState.description)
+ val intent = HomeActivity.newIntent(
+ activity,
+ firstStartMainActivity = false,
+ authenticationDescription = it.selectedAuthenticationState.description
+ )
activity.startActivity(intent)
activity.finish()
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
index 4649c7c799..eb00dc3e21 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
@@ -21,7 +21,7 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.view.isVisible
+import androidx.core.view.isInvisible
import com.airbnb.mvrx.args
import im.vector.app.R
import im.vector.app.core.utils.colorTerminatingFullStop
@@ -35,7 +35,8 @@ import javax.inject.Inject
@Parcelize
data class FtueAuthWaitForEmailFragmentArgument(
- val email: String
+ val email: String,
+ val isRestoredSession: Boolean,
) : Parcelable
/**
@@ -48,6 +49,8 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
private val params: FtueAuthWaitForEmailFragmentArgument by args()
private var inferHasLeftAndReturnedToScreen = false
+ override fun backIsHardExit() = params.isRestoredSession
+
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding {
return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false)
}
@@ -63,6 +66,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email)
views.emailVerificationResendEmail.debouncedClicks {
+ hideWaitingForVerificationLoading()
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid))
}
}
@@ -75,19 +79,32 @@ class FtueAuthWaitForEmailFragment @Inject constructor(
private fun showLoadingIfReturningToScreen() {
when (inferHasLeftAndReturnedToScreen) {
- true -> views.emailVerificationWaiting.isVisible = true
+ true -> showWaitingForVerificationLoading()
false -> {
inferHasLeftAndReturnedToScreen = true
}
}
}
+ private fun hideWaitingForVerificationLoading() {
+ views.emailVerificationWaiting.isInvisible = true
+ }
+
+ private fun showWaitingForVerificationLoading() {
+ views.emailVerificationWaiting.isInvisible = false
+ }
+
override fun onPause() {
super.onPause()
viewModel.handle(OnboardingAction.StopEmailValidationCheck)
}
override fun resetViewModel() {
- viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
+ when {
+ backIsHardExit() -> viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
+ else -> {
+ // delegate to the previous step
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt
index a34b284193..ae4fa637b4 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import java.security.KeyStore
+import javax.crypto.Cipher
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -74,22 +75,19 @@ class BiometricHelper @Inject constructor(
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
*/
val canUseWeakBiometricAuth: Boolean
- get() =
- configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
+ get() = configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
/**
* Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
*/
val canUseStrongBiometricAuth: Boolean
- get() =
- configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
+ get() = configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
/**
* Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.).
*/
val canUseDeviceCredentialsAuth: Boolean
- get() =
- configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
+ get() = configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
/**
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
@@ -120,7 +118,7 @@ class BiometricHelper @Inject constructor(
*/
@MainThread
fun enableAuthentication(activity: FragmentActivity): Flow {
- return authenticateInternal(activity, checkSystemKeyExists = false, cryptoObject = null)
+ return authenticateInternal(activity, checkSystemKeyExists = false, cryptoObject = getAuthCryptoObject())
}
/**
@@ -140,7 +138,7 @@ class BiometricHelper @Inject constructor(
*/
@MainThread
fun authenticate(activity: FragmentActivity): Flow {
- return authenticateInternal(activity, checkSystemKeyExists = true, cryptoObject = null)
+ return authenticateInternal(activity, checkSystemKeyExists = true, cryptoObject = getAuthCryptoObject())
}
/**
@@ -157,9 +155,9 @@ class BiometricHelper @Inject constructor(
@SuppressLint("NewApi")
@OptIn(ExperimentalCoroutinesApi::class)
private fun authenticateInternal(
- activity: FragmentActivity,
- checkSystemKeyExists: Boolean,
- cryptoObject: BiometricPrompt.CryptoObject? = null,
+ activity: FragmentActivity,
+ checkSystemKeyExists: Boolean,
+ cryptoObject: BiometricPrompt.CryptoObject,
): Flow {
if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false)
@@ -193,9 +191,9 @@ class BiometricHelper @Inject constructor(
@VisibleForTesting(otherwise = PRIVATE)
internal fun authenticateWithPromptInternal(
- activity: FragmentActivity,
- cryptoObject: BiometricPrompt.CryptoObject? = null,
- channel: Channel,
+ activity: FragmentActivity,
+ cryptoObject: BiometricPrompt.CryptoObject,
+ channel: Channel,
): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(context)
val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher())
@@ -214,15 +212,12 @@ class BiometricHelper @Inject constructor(
}
.setAllowedAuthenticators(authenticators)
.build()
+
return BiometricPrompt(activity, executor, callback).also {
showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) {
// For some reason this seems to be needed unless we want to receive a fragment transaction exception
delay(1L)
- if (cryptoObject != null) {
- it.authenticate(promptInfo, cryptoObject)
- } else {
- it.authenticate(promptInfo)
- }
+ it.authenticate(promptInfo, cryptoObject)
}
}
}
@@ -270,13 +265,28 @@ class BiometricHelper @Inject constructor(
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- scope.launch {
- channel.send(true)
- // Success is a terminal event, should close both the Channel and the CoroutineScope to free resources.
- channel.close()
- scope.cancel()
+ val cipher = result.cryptoObject?.cipher
+ if (isCipherValid(cipher)) {
+ scope.launch {
+ channel.send(true)
+ // Success is a terminal event, should close both the Channel and the CoroutineScope to free resources.
+ channel.close()
+ scope.cancel()
+ }
+ } else {
+ scope.launch {
+ channel.close(IllegalStateException("System key was not valid after authentication."))
+ scope.cancel()
+ }
}
}
+
+ private fun isCipherValid(cipher: Cipher?): Boolean {
+ if (cipher == null) return false
+ return runCatching {
+ cipher.doFinal("biometric_challenge".toByteArray())
+ }.isSuccess
+ }
}
/**
@@ -321,6 +331,9 @@ class BiometricHelper @Inject constructor(
@VisibleForTesting(otherwise = PRIVATE)
internal fun createAuthChannel(): Channel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun getAuthCryptoObject(): BiometricPrompt.CryptoObject = lockScreenKeyRepository.getSystemKeyAuthCryptoObject()
+
companion object {
private const val FALLBACK_BIOMETRIC_FRAGMENT_TAG = "fragment.biometric_fallback"
}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt
index 95266d7663..4c52fccbaa 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt
@@ -18,11 +18,11 @@ package im.vector.app.features.pin.lockscreen.crypto
import android.annotation.SuppressLint
import android.content.Context
-import android.hardware.biometrics.BiometricPrompt
import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.util.Base64
-import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.biometric.BiometricPrompt
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -40,8 +40,6 @@ class KeyStoreCrypto @AssistedInject constructor(
context: Context,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val keyStore: KeyStore,
- // It's easier to test it this way
- private val secretStoringUtils: SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider, keyNeedsUserAuthentication)
) {
@AssistedFactory
@@ -49,6 +47,9 @@ class KeyStoreCrypto @AssistedInject constructor(
fun provide(alias: String, keyNeedsUserAuthentication: Boolean): KeyStoreCrypto
}
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var secretStoringUtils: SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider, keyNeedsUserAuthentication)
+
/**
* Ensures a [Key] for the [alias] exists and validates it.
* @throws KeyPermanentlyInvalidatedException if key is not valid.
@@ -137,6 +138,5 @@ class KeyStoreCrypto @AssistedInject constructor(
* @throws KeyPermanentlyInvalidatedException if key is invalidated.
*/
@Throws(KeyPermanentlyInvalidatedException::class)
- @RequiresApi(Build.VERSION_CODES.P)
- fun getCryptoObject() = BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(alias))
+ fun getAuthCryptoObject() = BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(alias))
}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt
index 4a7bce8a52..2581951789 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt
@@ -19,22 +19,22 @@ package im.vector.app.features.pin.lockscreen.crypto
import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.annotation.RequiresApi
-import im.vector.app.features.settings.VectorPreferences
-import timber.log.Timber
+import androidx.biometric.BiometricPrompt
+import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
+import im.vector.app.features.pin.lockscreen.di.PinCodeKeyAlias
+import javax.inject.Inject
+import javax.inject.Singleton
/**
* Class in charge of managing both the PIN code key and the system authentication keys.
*/
-class LockScreenKeyRepository(
- baseName: String,
- private val pinCodeMigrator: PinCodeMigrator,
- private val vectorPreferences: VectorPreferences,
+@Singleton
+class LockScreenKeyRepository @Inject constructor(
+ @PinCodeKeyAlias private val pinCodeKeyAlias: String,
+ @BiometricKeyAlias private val systemKeyAlias: String,
private val keyStoreCryptoFactory: KeyStoreCrypto.Factory,
) {
- private val pinCodeKeyAlias = "$baseName.pin_code"
- private val systemKeyAlias = "$baseName.system"
-
private val pinCodeCrypto: KeyStoreCrypto by lazy {
keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false)
}
@@ -86,19 +86,7 @@ class LockScreenKeyRepository(
fun isSystemKeyValid() = systemKeyCrypto.hasValidKey()
/**
- * Migrates the PIN code key and encrypted value to use a more secure cipher, also creates a new system key if needed.
+ * Returns a [BiometricPrompt.CryptoObject] for the system key.
*/
- suspend fun migrateKeysIfNeeded() {
- if (pinCodeMigrator.isMigrationNeeded()) {
- pinCodeMigrator.migrate(pinCodeKeyAlias)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
- try {
- ensureSystemKey()
- } catch (e: KeyPermanentlyInvalidatedException) {
- Timber.e("Could not automatically create biometric key.", e)
- }
- }
- }
- }
+ fun getSystemKeyAuthCryptoObject() = systemKeyCrypto.getAuthCryptoObject()
}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt
new file mode 100644
index 0000000000..68acfcebf3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.pin.lockscreen.crypto
+
+import android.annotation.SuppressLint
+import android.os.Build
+import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
+import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator
+import im.vector.app.features.pin.lockscreen.crypto.migrations.SystemKeyV1Migrator
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+import javax.inject.Inject
+
+/**
+ * Performs all migrations needed for the lock screen keys.
+ */
+class LockScreenKeysMigrator @Inject constructor(
+ private val legacyPinCodeMigrator: LegacyPinCodeMigrator,
+ private val missingSystemKeyMigrator: MissingSystemKeyMigrator,
+ private val systemKeyV1Migrator: SystemKeyV1Migrator,
+ private val versionProvider: BuildVersionSdkIntProvider,
+) {
+ /**
+ * Performs any needed migrations in order.
+ */
+ @SuppressLint("NewApi")
+ suspend fun migrateIfNeeded() {
+ if (legacyPinCodeMigrator.isMigrationNeeded()) {
+ legacyPinCodeMigrator.migrate()
+ missingSystemKeyMigrator.migrate()
+ }
+
+ if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.get() >= Build.VERSION_CODES.M) {
+ systemKeyV1Migrator.migrate()
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigrator.kt
similarity index 88%
rename from vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigrator.kt
rename to vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigrator.kt
index 976ee48e5f..5d790ba01a 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigrator.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigrator.kt
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-package im.vector.app.features.pin.lockscreen.crypto
+package im.vector.app.features.pin.lockscreen.crypto.migrations
import android.os.Build
import android.util.Base64
import androidx.annotation.VisibleForTesting
import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS
+import im.vector.app.features.pin.lockscreen.di.PinCodeKeyAlias
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import java.security.Key
@@ -31,7 +32,8 @@ import javax.inject.Inject
/**
* Used to migrate from the old PIN code key ciphers to a more secure ones.
*/
-class PinCodeMigrator @Inject constructor(
+class LegacyPinCodeMigrator @Inject constructor(
+ @PinCodeKeyAlias private val pinCodeKeyAlias: String,
private val pinCodeStore: PinCodeStore,
private val keyStore: KeyStore,
private val secretStoringUtils: SecretStoringUtils,
@@ -41,13 +43,13 @@ class PinCodeMigrator @Inject constructor(
private val legacyKey: Key get() = keyStore.getKey(LEGACY_PIN_CODE_KEY_ALIAS, null)
/**
- * Migrates from the old ciphers and [LEGACY_PIN_CODE_KEY_ALIAS] to the [newAlias].
+ * Migrates from the old ciphers and renames [LEGACY_PIN_CODE_KEY_ALIAS] to [pinCodeKeyAlias].
*/
- suspend fun migrate(newAlias: String) {
+ suspend fun migrate() {
if (!keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
val pinCode = getDecryptedPinCode() ?: return
- val encryptedBytes = secretStoringUtils.securelyStoreBytes(pinCode.toByteArray(), newAlias)
+ val encryptedBytes = secretStoringUtils.securelyStoreBytes(pinCode.toByteArray(), pinCodeKeyAlias)
val encryptedPinCode = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
pinCodeStore.savePinCode(encryptedPinCode)
keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS)
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt
new file mode 100644
index 0000000000..f8d3520047
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt
@@ -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.features.pin.lockscreen.crypto.migrations
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
+import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
+import im.vector.app.features.settings.VectorPreferences
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Creates a new system/biometric key when migrating from the old PFLockScreen implementation.
+ */
+class MissingSystemKeyMigrator @Inject constructor(
+ @BiometricKeyAlias private val systemKeyAlias: String,
+ private val keystoreCryptoFactory: KeyStoreCrypto.Factory,
+ private val vectorPreferences: VectorPreferences,
+ private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+) {
+
+ /**
+ * If user had biometric auth enabled, ensure system key exists, creating one if needed.
+ */
+ @SuppressLint("NewApi")
+ fun migrate() {
+ if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
+ try {
+ keystoreCryptoFactory.provide(systemKeyAlias, true).ensureKey()
+ } catch (e: KeyPermanentlyInvalidatedException) {
+ Timber.e("Could not automatically create biometric key.", e)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1Migrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1Migrator.kt
new file mode 100644
index 0000000000..25e9d24272
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1Migrator.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.pin.lockscreen.crypto.migrations
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
+import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
+import java.security.KeyStore
+import javax.inject.Inject
+
+/**
+ * Migrates from the v1 of the biometric/system key to the new format, adding extra security measures to the new key.
+ */
+class SystemKeyV1Migrator @Inject constructor(
+ @BiometricKeyAlias private val systemKeyAlias: String,
+ private val keyStore: KeyStore,
+ private val keystoreCryptoFactory: KeyStoreCrypto.Factory,
+) {
+
+ /**
+ * Removes the old v1 system key and creates a new system key.
+ */
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun migrate() {
+ keyStore.deleteEntry(SYSTEM_KEY_ALIAS_V1)
+ val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true)
+ systemKeyStoreCrypto.ensureKey()
+ }
+
+ /**
+ * Checks if an entry with [SYSTEM_KEY_ALIAS_V1] exists in the [keyStore].
+ */
+ fun isMigrationNeeded() = keyStore.containsAlias(SYSTEM_KEY_ALIAS_V1)
+
+ companion object {
+ internal const val SYSTEM_KEY_ALIAS_V1 = "vector.system"
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenModule.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenModule.kt
index ef8f03b9e5..fb333b96bb 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenModule.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenModule.kt
@@ -30,35 +30,32 @@ import im.vector.app.core.di.MavericksViewModelKey
import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
-import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
-import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
-import im.vector.app.features.pin.lockscreen.crypto.PinCodeMigrator
+import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.pin.lockscreen.pincode.EncryptedPinCodeStorage
import im.vector.app.features.pin.lockscreen.ui.LockScreenViewModel
-import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
import java.security.KeyStore
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object LockScreenModule {
+ @Provides
+ @PinCodeKeyAlias
+ fun providePinCodeKeyAlias(): String = "vector.pin_code"
+
+ @Provides
+ @BiometricKeyAlias
+ fun provideSystemKeyAlias(): String = "vector.system_v2"
+
@Provides
fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
@Provides
fun provideBuildVersionSdkIntProvider(): BuildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider()
- @Provides
- fun provideSecretStoringUtils(
- @ApplicationContext context: Context,
- keyStore: KeyStore,
- buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
- ) = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
-
@Provides
fun provideLockScreenConfig() = LockScreenConfiguration(
mode = LockScreenMode.VERIFY,
@@ -70,20 +67,22 @@ object LockScreenModule {
)
@Provides
- @Singleton
- fun provideKeyRepository(
- pinCodeMigrator: PinCodeMigrator,
- vectorPreferences: VectorPreferences,
- keyStoreCryptoFactory: KeyStoreCrypto.Factory,
- ) = LockScreenKeyRepository(
- baseName = "vector",
- pinCodeMigrator,
- vectorPreferences,
- keyStoreCryptoFactory,
- )
+ fun provideBiometricManager(@ApplicationContext context: Context) = BiometricManager.from(context)
@Provides
- fun provideBiometricManager(@ApplicationContext context: Context) = BiometricManager.from(context)
+ fun provideLegacyPinCodeMigrator(
+ @PinCodeKeyAlias pinCodeKeyAlias: String,
+ context: Context,
+ pinCodeStore: PinCodeStore,
+ keyStore: KeyStore,
+ buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+ ) = LegacyPinCodeMigrator(
+ pinCodeKeyAlias,
+ pinCodeStore,
+ keyStore,
+ SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider),
+ buildVersionSdkIntProvider,
+ )
}
@Module
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenQualifiers.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenQualifiers.kt
new file mode 100644
index 0000000000..0954772772
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/di/LockScreenQualifiers.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.pin.lockscreen.di
+
+import javax.inject.Qualifier
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class PinCodeKeyAlias
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class BiometricKeyAlias
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/pincode/PinCodeHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/pincode/PinCodeHelper.kt
index bb2861dcda..9b2c2efda5 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/pincode/PinCodeHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/pincode/PinCodeHelper.kt
@@ -55,11 +55,4 @@ class PinCodeHelper @Inject constructor(
encryptedStorage.deletePinCode()
lockScreenKeyRepository.deletePinCodeKey()
}
-
- /**
- * Migrates the PIN code key and encrypted value to use a more secure cipher.
- */
- suspend fun migratePinCodeIfNeeded() {
- lockScreenKeyRepository.migrateKeysIfNeeded()
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt
index 39d0937323..79c1967670 100644
--- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt
@@ -34,6 +34,7 @@ import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
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.LockScreenKeysMigrator
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
@@ -47,6 +48,7 @@ class LockScreenViewModel @AssistedInject constructor(
@Assisted val initialState: LockScreenViewState,
private val pinCodeHelper: PinCodeHelper,
private val biometricHelper: BiometricHelper,
+ private val lockScreenKeysMigrator: LockScreenKeysMigrator,
private val configuratorProvider: LockScreenConfiguratorProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : VectorViewModel(initialState) {
@@ -85,7 +87,7 @@ class LockScreenViewModel @AssistedInject constructor(
init {
// We need this to run synchronously before we start reading the configurations
- runBlocking { pinCodeHelper.migratePinCodeIfNeeded() }
+ runBlocking { lockScreenKeysMigrator.migrateIfNeeded() }
configuratorProvider.configurationFlow
.onEach { updateConfiguration(it) }
diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
index 3ebcb3f318..40ef6d819e 100644
--- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
@@ -26,6 +26,7 @@ import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.time.Clock
import im.vector.app.core.utils.isAnimationEnabled
+import im.vector.app.features.MainActivity
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
import im.vector.app.features.pin.PinActivity
import im.vector.app.features.signout.hard.SignedOutActivity
@@ -302,6 +303,7 @@ class PopupAlertManager @Inject constructor(
private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean {
return alert != null &&
+ activity !is MainActivity &&
activity !is PinActivity &&
activity !is SignedOutActivity &&
activity !is AnalyticsOptInActivity &&
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 7bee66d722..587e6a59b4 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -206,6 +206,8 @@ class VectorPreferences @Inject constructor(
private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION"
+ private const val SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS = "SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS"
+
// This key will be used to identify clients with the old thread support enabled io.element.thread
const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES"
@@ -1050,6 +1052,10 @@ class VectorPreferences @Inject constructor(
}
}
+ fun labsEnableElementCallPermissionShortcuts(): Boolean {
+ return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS, false)
+ }
+
/**
* Indicates whether or not thread messages are enabled.
*/
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt
index 85abf846fa..c91c0f457b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt
@@ -106,6 +106,18 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor(
.toEpoxyCharSequence()
)
}
+ } else {
+ genericItem {
+ id("reset${cryptoDeviceInfo.deviceId}")
+ style(ItemStyle.BIG_TEXT)
+ titleIconResourceId(shield)
+ title(host.stringProvider.getString(R.string.crosssigning_cannot_verify_this_session).toEpoxyCharSequence())
+ description(
+ host.stringProvider
+ .getString(R.string.crosssigning_cannot_verify_this_session_desc)
+ .toEpoxyCharSequence()
+ )
+ }
}
} else {
if (!currentSessionIsTrusted) {
@@ -141,22 +153,42 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor(
description("(${cryptoDeviceInfo.deviceId})".toEpoxyCharSequence())
}
- if (isMine && !currentSessionIsTrusted && data.canVerifySession) {
- // Add complete security
- bottomSheetDividerItem {
- id("completeSecurityDiv")
- }
- bottomSheetVerificationActionItem {
- id("completeSecurity")
- title(host.stringProvider.getString(R.string.crosssigning_verify_this_session))
- titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
- iconRes(R.drawable.ic_arrow_right)
- iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
- listener {
- host.callback?.onAction(DevicesAction.CompleteSecurity)
+ if (isMine) {
+ if (!currentSessionIsTrusted) {
+ if (data.canVerifySession) {
+ // Add complete security
+ bottomSheetDividerItem {
+ id("completeSecurityDiv")
+ }
+ bottomSheetVerificationActionItem {
+ id("completeSecurity")
+ title(host.stringProvider.getString(R.string.crosssigning_verify_this_session))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ listener {
+ host.callback?.onAction(DevicesAction.CompleteSecurity)
+ }
+ }
+ } else {
+ bottomSheetDividerItem {
+ id("resetSecurityDiv")
+ }
+ bottomSheetVerificationActionItem {
+ id("resetSecurity")
+ title(host.stringProvider.getString(R.string.secure_backup_reset_all))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ listener {
+ host.callback?.onAction(DevicesAction.ResetSecurity)
+ }
+ }
}
}
- } else if (!isMine) {
+ } else
+ /** if (!isMine) **/
+ {
if (currentSessionIsTrusted) {
// we can propose to verify it
val isVerified = cryptoDeviceInfo.trustLevel?.crossSigningVerified.orFalse()
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt
index 8ee0e7636e..b915236329 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt
@@ -30,6 +30,7 @@ sealed class DevicesAction : VectorViewModelAction {
data class VerifyMyDevice(val deviceId: String) : DevicesAction()
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
object CompleteSecurity : DevicesAction()
+ object ResetSecurity : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
object SsoAuthDone : DevicesAction()
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt
index c057e2b565..c97f353110 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt
@@ -48,4 +48,6 @@ sealed class DevicesViewEvents : VectorViewEvents {
) : DevicesViewEvents()
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()
+
+ object PromptResetSecrets : DevicesViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
index 1d9f2a97c2..1840a97098 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
@@ -239,6 +239,7 @@ class DevicesViewModel @AssistedInject constructor(
uiaContinuation = null
pendingAuth = null
}
+ DevicesAction.ResetSecurity -> _viewEvents.post(DevicesViewEvents.PromptResetSecrets)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt
index 0c52099f92..a132dc1f49 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt
@@ -37,6 +37,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
+import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
@@ -89,6 +90,9 @@ class VectorSettingsDevicesFragment @Inject constructor(
viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo))
}
}
+ is DevicesViewEvents.PromptResetSecrets -> {
+ navigator.open4SSetup(requireContext(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
+ }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt
index 4e9f024b15..70be2b2b6d 100644
--- a/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt
+++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
sealed class IncomingShareAction : VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction()
object ShareToSelectedRooms : IncomingShareAction()
- data class ShareToRoom(val roomSummary: RoomSummary) : IncomingShareAction()
+ data class ShareToRoom(val roomId: String) : IncomingShareAction()
data class ShareMedia(val keepOriginalSize: Boolean) : IncomingShareAction()
data class FilterWith(val filter: String) : IncomingShareAction()
data class UpdateSharedData(val sharedData: SharedData) : IncomingShareAction()
diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt
index 439d9b64fa..3d603e3f6a 100644
--- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt
@@ -16,21 +16,66 @@
package im.vector.app.features.share
+import android.content.Intent
+import android.os.Bundle
+import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
+import im.vector.app.features.MainActivity
+import im.vector.app.features.start.StartAppViewModel
+import javax.inject.Inject
@AndroidEntryPoint
class IncomingShareActivity : VectorBaseActivity() {
+ private val startAppViewModel: StartAppViewModel by viewModel()
+
+ @Inject lateinit var activeSessionHolder: ActiveSessionHolder
+
+ private val launcher = registerStartForActivityResult {
+ if (it.resultCode == RESULT_OK) {
+ handleAppStarted()
+ } else {
+ // User has pressed back on the MainActivity, so finish also this one.
+ finish()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (startAppViewModel.shouldStartApp()) {
+ launcher.launch(MainActivity.getIntentToInitSession(this))
+ } else {
+ handleAppStarted()
+ }
+ }
+
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun getCoordinatorLayout() = views.coordinatorLayout
- override fun initUiAndData() {
- if (isFirstCreation()) {
- addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java)
+ private fun handleAppStarted() {
+ // If we are not logged in, stop the sharing process and open login screen.
+ // In the future, we might want to relaunch the sharing process after login.
+ if (!activeSessionHolder.hasActiveSession()) {
+ startLoginActivity()
+ } else {
+ if (isFirstCreation()) {
+ addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java)
+ }
}
}
+
+ private fun startLoginActivity() {
+ navigator.openLogin(
+ context = this,
+ flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+ )
+ finish()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt
index 3f8923dd68..3e2ddc469c 100644
--- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt
@@ -30,7 +30,6 @@ 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.di.ActiveSessionHolder
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult
@@ -40,7 +39,6 @@ import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.attachments.ShareIntentHandler
import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity
import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
-import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
@@ -50,7 +48,6 @@ import javax.inject.Inject
*/
class IncomingShareFragment @Inject constructor(
private val incomingShareController: IncomingShareController,
- private val sessionHolder: ActiveSessionHolder,
private val shareIntentHandler: ShareIntentHandler,
) :
VectorBaseFragment(),
@@ -63,12 +60,6 @@ class IncomingShareFragment @Inject constructor(
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- // If we are not logged in, stop the sharing process and open login screen.
- // In the future, we might want to relaunch the sharing process after login.
- if (!sessionHolder.hasActiveSession()) {
- startLoginActivity()
- return
- }
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupToolbar(views.incomingShareToolbar)
@@ -88,7 +79,7 @@ class IncomingShareFragment @Inject constructor(
// Direct share
if (intent.hasExtra(Intent.EXTRA_SHORTCUT_ID)) {
val roomId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)!!
- sessionHolder.getSafeActiveSession()?.getRoomSummary(roomId)?.let { viewModel.handle(IncomingShareAction.ShareToRoom(it)) }
+ viewModel.handle(IncomingShareAction.ShareToRoom(roomId))
}
isShareManaged
}
@@ -192,14 +183,6 @@ class IncomingShareFragment @Inject constructor(
.show()
}
- private fun startLoginActivity() {
- navigator.openLogin(
- context = requireActivity(),
- flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
- )
- requireActivity().finish()
- }
-
override fun invalidate() = withState(viewModel) {
views.sendShareButton.isVisible = it.isInMultiSelectionMode
incomingShareController.setData(it)
diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt
index 1191fd04e8..85629ea150 100644
--- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
@@ -134,7 +135,8 @@ class IncomingShareViewModel @AssistedInject constructor(
private fun handleShareToRoom(action: IncomingShareAction.ShareToRoom) = withState { state ->
val sharedData = state.sharedData ?: return@withState
- _viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, showAlert = false))
+ val roomSummary = session.getRoomSummary(action.roomId) ?: return@withState
+ _viewEvents.post(IncomingShareViewEvents.ShareToRoom(roomSummary, sharedData, showAlert = false))
}
private fun handleShareMediaToSelectedRooms(action: IncomingShareAction.ShareMedia) = withState { state ->
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt
new file mode 100644
index 0000000000..fffb124f12
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.start
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface StartAppAction : VectorViewModelAction {
+ object StartApp : StartAppAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt
new file mode 100644
index 0000000000..e8e0eac863
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.start
+
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.di.NamedGlobalScope
+import im.vector.app.core.services.VectorAndroidService
+import im.vector.app.features.notifications.NotificationUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import kotlin.random.Random
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * A simple foreground service that let the app (and the SDK) time to initialize.
+ * Will self stop itself once the active session is set.
+ */
+@AndroidEntryPoint
+class StartAppAndroidService : VectorAndroidService() {
+
+ @NamedGlobalScope @Inject lateinit var globalScope: CoroutineScope
+ @Inject lateinit var notificationUtils: NotificationUtils
+ @Inject lateinit var activeSessionHolder: ActiveSessionHolder
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ showStickyNotification()
+ startPollingActiveSession()
+ return START_STICKY
+ }
+
+ private fun startPollingActiveSession() {
+ globalScope.launch {
+ do {
+ delay(1.seconds.inWholeMilliseconds)
+ } while (activeSessionHolder.hasActiveSession().not())
+ myStopSelf()
+ }
+ }
+
+ private fun showStickyNotification() {
+ val notificationId = Random.nextInt()
+ val notification = notificationUtils.buildStartAppNotification()
+ startForeground(notificationId, notification)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt
new file mode 100644
index 0000000000..986d41f983
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.start
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed interface StartAppViewEvent : VectorViewEvents {
+ /**
+ * Will be sent if the process is taking more than 1 second.
+ */
+ object StartForegroundService : StartAppViewEvent
+
+ /**
+ * Will be sent when the current Session has been set.
+ */
+ object AppStarted : StartAppViewEvent
+}
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt
new file mode 100644
index 0000000000..62a7517f5a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.start
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.ActiveSessionSetter
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+class StartAppViewModel @AssistedInject constructor(
+ @Assisted val initialState: StartAppViewState,
+ private val activeSessionSetter: ActiveSessionSetter,
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: StartAppViewState): StartAppViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ fun shouldStartApp(): Boolean {
+ return activeSessionSetter.shouldSetActionSession()
+ }
+
+ override fun handle(action: StartAppAction) {
+ when (action) {
+ StartAppAction.StartApp -> handleStartApp()
+ }
+ }
+
+ private fun handleStartApp() {
+ handleLongProcessing()
+ viewModelScope.launch(Dispatchers.IO) {
+ // This can take time because of DB migration(s), so do it in a background task.
+ activeSessionSetter.tryToSetActiveSession(startSync = true)
+ _viewEvents.post(StartAppViewEvent.AppStarted)
+ }
+ }
+
+ private fun handleLongProcessing() {
+ viewModelScope.launch(Dispatchers.Default) {
+ delay(1.seconds.inWholeMilliseconds)
+ setState { copy(mayBeLongToProcess = true) }
+ _viewEvents.post(StartAppViewEvent.StartForegroundService)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt
new file mode 100644
index 0000000000..3ff933f054
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.start
+
+import com.airbnb.mvrx.MavericksState
+
+data class StartAppViewState(
+ val mayBeLongToProcess: Boolean = false
+) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt
index b72ea68b7f..d5d8e95aa6 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt
@@ -26,4 +26,5 @@ sealed class WidgetAction : VectorViewModelAction {
object DeleteWidget : WidgetAction()
object RevokeWidget : WidgetAction()
object OnTermsReviewed : WidgetAction()
+ object CloseWidget : WidgetAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt
index 954f622801..91aa3a4e6a 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt
@@ -17,8 +17,20 @@
package im.vector.app.features.widgets
import android.app.Activity
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.app.PictureInPictureParams
+import android.app.RemoteAction
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.util.Rational
+import androidx.annotation.RequiresApi
+import androidx.core.app.PictureInPictureModeChangedInfo
+import androidx.core.util.Consumer
import androidx.core.view.isVisible
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
@@ -27,11 +39,14 @@ import im.vector.app.R
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityWidgetBinding
+import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewEvents
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Content
import java.io.Serializable
+import javax.inject.Inject
@AndroidEntryPoint
class WidgetActivity : VectorBaseActivity() {
@@ -40,6 +55,10 @@ class WidgetActivity : VectorBaseActivity() {
private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG"
private const val WIDGET_PERMISSION_FRAGMENT_TAG = "WIDGET_PERMISSION_FRAGMENT_TAG"
private const val EXTRA_RESULT = "EXTRA_RESULT"
+ private const val REQUEST_CODE_HANGUP = 1
+ private const val ACTION_MEDIA_CONTROL = "MEDIA_CONTROL"
+ private const val EXTRA_CONTROL_TYPE = "EXTRA_CONTROL_TYPE"
+ private const val CONTROL_TYPE_HANGUP = 2
fun newIntent(context: Context, args: WidgetArgs): Intent {
return Intent(context, WidgetActivity::class.java).apply {
@@ -62,6 +81,8 @@ class WidgetActivity : VectorBaseActivity() {
private val viewModel: WidgetViewModel by viewModel()
private val permissionViewModel: RoomWidgetPermissionViewModel by viewModel()
+ @Inject lateinit var vectorPreferences: VectorPreferences
+
override fun getBinding() = ActivityWidgetBinding.inflate(layoutInflater)
override fun getTitleRes() = R.string.room_widget_activity_title
@@ -82,29 +103,37 @@ class WidgetActivity : VectorBaseActivity() {
}
}
- permissionViewModel.observeViewEvents {
- when (it) {
- is RoomWidgetPermissionViewEvents.Close -> finish()
+ // Trust element call widget by default
+ if (widgetArgs.kind == WidgetKind.ELEMENT_CALL && vectorPreferences.labsEnableElementCallPermissionShortcuts()) {
+ if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) {
+ addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer)
+ addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG)
+ }
+ } else {
+ permissionViewModel.observeViewEvents {
+ when (it) {
+ is RoomWidgetPermissionViewEvents.Close -> finish()
+ }
}
- }
- viewModel.onEach(WidgetViewState::status) { ws ->
- when (ws) {
- WidgetStatus.UNKNOWN -> {
- }
- WidgetStatus.WIDGET_NOT_ALLOWED -> {
- val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet
- if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) {
- return@onEach
- } else {
- RoomWidgetPermissionBottomSheet
- .newInstance(widgetArgs)
- .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG)
+ viewModel.onEach(WidgetViewState::status) { ws ->
+ when (ws) {
+ WidgetStatus.UNKNOWN -> {
}
- }
- WidgetStatus.WIDGET_ALLOWED -> {
- if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) {
- addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG)
+ WidgetStatus.WIDGET_NOT_ALLOWED -> {
+ val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet
+ if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) {
+ return@onEach
+ } else {
+ RoomWidgetPermissionBottomSheet
+ .newInstance(widgetArgs)
+ .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG)
+ }
+ }
+ WidgetStatus.WIDGET_ALLOWED -> {
+ if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) {
+ addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG)
+ }
}
}
}
@@ -119,6 +148,64 @@ class WidgetActivity : VectorBaseActivity() {
}
}
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG)
+ if (widgetArgs?.kind?.supportsPictureInPictureMode().orFalse()) {
+ enterPictureInPicture()
+ }
+ }
+
+ override fun onDestroy() {
+ removeOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer)
+ super.onDestroy()
+ }
+
+ private fun enterPictureInPicture() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createElementCallPipParams()?.let {
+ enterPictureInPictureMode(it)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createElementCallPipParams(): PictureInPictureParams? {
+ val actions = mutableListOf()
+ val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_HANGUP)
+ val pendingIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_HANGUP, intent, FLAG_IMMUTABLE)
+ val icon = Icon.createWithResource(this, R.drawable.ic_call_hangup)
+ actions.add(RemoteAction(icon, getString(R.string.call_notification_hangup), getString(R.string.call_notification_hangup), pendingIntent))
+
+ val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height))
+ return PictureInPictureParams.Builder()
+ .setAspectRatio(aspectRatio)
+ .setActions(actions)
+ .build()
+ }
+
+ private var hangupBroadcastReceiver: BroadcastReceiver? = null
+
+ private val pictureInPictureModeChangedInfoConsumer = Consumer {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return@Consumer
+
+ if (isInPictureInPictureMode) {
+ hangupBroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == ACTION_MEDIA_CONTROL) {
+ val controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)
+ if (controlType == CONTROL_TYPE_HANGUP) {
+ viewModel.handle(WidgetAction.CloseWidget)
+ }
+ }
+ }
+ }
+ registerReceiver(hangupBroadcastReceiver, IntentFilter(ACTION_MEDIA_CONTROL))
+ } else {
+ unregisterReceiver(hangupBroadcastReceiver)
+ }
+ }
+
private fun handleClose(event: WidgetViewEvents.Close) {
if (event.content != null) {
val intent = createResultIntent(event.content)
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt
index 777bd9cc7e..83ea100cb6 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt
@@ -78,6 +78,13 @@ class WidgetArgsBuilder @Inject constructor(
)
}
+ fun buildElementCallWidgetArgs(roomId: String, widget: Widget): WidgetArgs {
+ return buildRoomWidgetArgs(roomId, widget)
+ .copy(
+ kind = WidgetKind.ELEMENT_CALL
+ )
+ }
+
@Suppress("UNCHECKED_CAST")
private fun Map.filterNotNull(): Map {
return filterValues { it != null } as Map
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt
index 5501031e92..ed1bace70c 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt
@@ -43,8 +43,10 @@ import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider
+import im.vector.app.core.utils.CheckWebViewPermissionsUseCase
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentRoomWidgetBinding
+import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.webview.WebEventListener
import im.vector.app.features.widgets.webview.WebviewPermissionUtils
import im.vector.app.features.widgets.webview.clearAfterWidget
@@ -65,7 +67,9 @@ data class WidgetArgs(
) : Parcelable
class WidgetFragment @Inject constructor(
- private val permissionUtils: WebviewPermissionUtils
+ private val permissionUtils: WebviewPermissionUtils,
+ private val checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase,
+ private val vectorPreferences: VectorPreferences,
) :
VectorBaseFragment(),
WebEventListener,
@@ -81,7 +85,7 @@ class WidgetFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- views.widgetWebView.setupForWidget(this)
+ views.widgetWebView.setupForWidget(requireActivity(), checkWebViewPermissionsUseCase, this)
if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().setWebView(views.widgetWebView)
}
@@ -131,9 +135,11 @@ class WidgetFragment @Inject constructor(
override fun onPause() {
super.onPause()
- views.widgetWebView.let {
- it.pauseTimers()
- it.onPause()
+ if (fragmentArgs.kind != WidgetKind.ELEMENT_CALL) {
+ views.widgetWebView.let {
+ it.pauseTimers()
+ it.onPause()
+ }
}
}
@@ -298,7 +304,8 @@ class WidgetFragment @Inject constructor(
request = request,
context = requireContext(),
activity = requireActivity(),
- activityResultLauncher = permissionResultLauncher
+ activityResultLauncher = permissionResultLauncher,
+ autoApprove = fragmentArgs.kind == WidgetKind.ELEMENT_CALL && vectorPreferences.labsEnableElementCallPermissionShortcuts()
)
}
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt
index b3f4712815..ecd6ca2fd6 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt
@@ -147,9 +147,14 @@ class WidgetViewModel @AssistedInject constructor(
WidgetAction.DeleteWidget -> handleDeleteWidget()
WidgetAction.RevokeWidget -> handleRevokeWidget()
WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false)
+ WidgetAction.CloseWidget -> handleCloseWidget()
}
}
+ private fun handleCloseWidget() {
+ _viewEvents.post(WidgetViewEvents.Close())
+ }
+
private fun handleRevokeWidget() {
viewModelScope.launch {
val widgetId = initialState.widgetId ?: return@launch
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt
index 2d98f734dd..cd2ed23980 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt
@@ -33,11 +33,16 @@ enum class WidgetStatus {
enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) {
ROOM(R.string.room_widget_activity_title, null),
STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred),
- INTEGRATION_MANAGER(0, null);
+ INTEGRATION_MANAGER(0, null),
+ ELEMENT_CALL(0, null);
fun isAdmin(): Boolean {
return this == STICKER_PICKER || this == INTEGRATION_MANAGER
}
+
+ fun supportsPictureInPictureMode(): Boolean {
+ return this == ELEMENT_CALL
+ }
}
data class WidgetViewState(
diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt
index fa7b842ab9..44af4ec335 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt
@@ -41,11 +41,22 @@ class WebviewPermissionUtils @Inject constructor(
request: PermissionRequest,
context: Context,
activity: FragmentActivity,
- activityResultLauncher: ActivityResultLauncher>
+ activityResultLauncher: ActivityResultLauncher>,
+ autoApprove: Boolean = false
) {
+ if (autoApprove) {
+ onPermissionsSelected(
+ permissions = request.resources.toList(),
+ request = request,
+ activity = activity,
+ activityResultLauncher = activityResultLauncher)
+ return
+ }
+
val allowedPermissions = request.resources.map {
it to false
}.toMutableList()
+
MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMultiChoiceItems(
@@ -54,21 +65,10 @@ class WebviewPermissionUtils @Inject constructor(
allowedPermissions[which] = allowedPermissions[which].first to isChecked
}
.setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ ->
- permissionRequest = request
- selectedPermissions = allowedPermissions.mapNotNull { perm ->
+ val permissions = allowedPermissions.mapNotNull { perm ->
perm.first.takeIf { perm.second }
}
-
- val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission ->
- webPermissionToAndroidPermission(permission)
- }
-
- // When checkPermissions returns false, some of the required Android permissions will
- // have to be requested and the flow completes asynchronously via onPermissionResult
- if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) {
- request.grant(selectedPermissions.toTypedArray())
- reset()
- }
+ onPermissionsSelected(permissions, request, activity, activityResultLauncher)
}
.setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ ->
request.deny()
@@ -76,6 +76,27 @@ class WebviewPermissionUtils @Inject constructor(
.show()
}
+ private fun onPermissionsSelected(
+ permissions: List,
+ request: PermissionRequest,
+ activity: FragmentActivity,
+ activityResultLauncher: ActivityResultLauncher>,
+ ) {
+ permissionRequest = request
+ selectedPermissions = permissions
+
+ val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission ->
+ webPermissionToAndroidPermission(permission)
+ }
+
+ // When checkPermissions returns false, some of the required Android permissions will
+ // have to be requested and the flow completes asynchronously via onPermissionResult
+ if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) {
+ request.grant(selectedPermissions.toTypedArray())
+ reset()
+ }
+ }
+
fun onPermissionResult(result: Map) {
if (permissionRequest == null) {
fatalError(
diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt
index 0207987ca3..ac9930866f 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt
@@ -17,18 +17,23 @@
package im.vector.app.features.widgets.webview
import android.annotation.SuppressLint
+import android.app.Activity
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
import im.vector.app.R
+import im.vector.app.core.utils.CheckWebViewPermissionsUseCase
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.webview.VectorWebViewClient
import im.vector.app.features.webview.WebEventListener
@SuppressLint("NewApi")
-fun WebView.setupForWidget(eventListener: WebEventListener) {
+fun WebView.setupForWidget(activity: Activity,
+ checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase,
+ eventListener: WebEventListener,
+) {
// xml value seems ignored
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
@@ -56,10 +61,16 @@ fun WebView.setupForWidget(eventListener: WebEventListener) {
settings.displayZoomControls = false
+ settings.mediaPlaybackRequiresUserGesture = false
+
// Permission requests
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
- eventListener.onPermissionRequest(request)
+ if (checkWebViewPermissionsUseCase.execute(activity, request)) {
+ request.grant(request.resources)
+ } else {
+ eventListener.onPermissionRequest(request)
+ }
}
}
webViewClient = VectorWebViewClient(eventListener)
diff --git a/vector/src/main/res/drawable/ic_robot.xml b/vector/src/main/res/drawable/ic_robot.xml
new file mode 100644
index 0000000000..cdc358f7eb
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_robot.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/layout/activity_main.xml b/vector/src/main/res/layout/activity_main.xml
index c7bca50acb..ba5925f000 100644
--- a/vector/src/main/res/layout/activity_main.xml
+++ b/vector/src/main/res/layout/activity_main.xml
@@ -1,5 +1,4 @@
-
-
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml
index b23ea9d7cc..9533ab29fc 100644
--- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml
+++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml
@@ -66,7 +66,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
- android:text="@string/ftue_auth_create_account_choose_server_header"
+ android:text="@string/ftue_auth_sign_in_choose_server_header"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/selectedServerName"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
diff --git a/vector/src/main/res/layout/fragment_ftue_login_captcha.xml b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml
index becb745305..2f6970c785 100644
--- a/vector/src/main/res/layout/fragment_ftue_login_captcha.xml
+++ b/vector/src/main/res/layout/fragment_ftue_login_captcha.xml
@@ -38,7 +38,7 @@
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
- android:src="@drawable/ic_user_fg"
+ android:src="@drawable/ic_robot"
app:layout_constraintBottom_toTopOf="@id/captchaHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
app:layout_constraintHeight_percent="0.10"
diff --git a/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml
index afe7a06183..f1944e25ad 100644
--- a/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml
+++ b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml
@@ -1,6 +1,7 @@
+ app:layout_constraintTop_toBottomOf="@id/chooseServerHeaderTitle"
+ tools:text="@string/ftue_auth_choose_server_subtitle" />
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="center" />
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml b/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml
index d5a0cefb28..53b740bb4e 100644
--- a/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml
@@ -15,16 +15,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
+ app:locLiveEndedBkgWithAlpha="true"
+ app:locLiveEndedIconMarginStart="8dp" />
-
-
-
-
diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml
index a696140669..3875a25773 100644
--- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml
@@ -45,8 +45,8 @@
app:layout_constraintTop_toBottomOf="@id/staticMapPinImageView"
tools:visibility="visible" />
-
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_location_live_message_banner.xml b/vector/src/main/res/layout/view_location_live_running_banner.xml
similarity index 63%
rename from vector/src/main/res/layout/view_location_live_message_banner.xml
rename to vector/src/main/res/layout/view_location_live_running_banner.xml
index 5c8f3a8970..5bf8c8258d 100644
--- a/vector/src/main/res/layout/view_location_live_message_banner.xml
+++ b/vector/src/main/res/layout/view_location_live_running_banner.xml
@@ -7,7 +7,7 @@
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+ app:layout_constraintTop_toTopOf="@id/locationLiveRunningBannerBackground" />
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 841ddc1c35..770df1f770 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -524,6 +524,7 @@
This homeserver would like to make sure you are not a robot
The email address linked to your account must be entered.
Failed to verify email address: make sure you clicked the link in the email
+ Email not verified, check your inbox
"Please review and accept the policies of this homeserver:"
@@ -764,6 +765,11 @@
Filter room members
Filter banned users
No results
+ %1$s and %2$s
+
+ - %1$s and %2$d other
+ - %1$s and %2$d others
+
All messages
@@ -1620,6 +1626,7 @@
No network. Please check your Internet connection.
"Change network"
"Please wait…"
+ Updating your data…
"All Communities"
@@ -1923,6 +1930,7 @@
Others can discover you %s
Must be 8 characters or more
Where your conversations will live
+ Where your conversations live
Or
Edit
@@ -1930,6 +1938,7 @@
Select your server
What is the address of your server? This is like a home for all your data
+ What is the address of your server?
Server URL
Want to host your own server?
@@ -1938,7 +1947,7 @@
Server policies
- Please read through %s\'s terns and policies
+ Please read through %s\'s terms and policies
Enter your email
@@ -1964,9 +1973,9 @@
A code was sent to %s
Resend code
- Check your email to verify.
+ Verify your email
- To confirm your email, tap the button in the email we just sent to %s
+ Follow the instructions sent to %s
Did not receive an email?
Resend email
Forgot password
@@ -2346,7 +2355,9 @@
- %d active sessions
- Verify this login
+ Verify this device
+ Unable to verify this device
+ You won’t be able to access encrypted message history. Reset your Secure Message Backup and verification keys to start fresh.
Use an existing session to verify this one, granting it access to encrypted messages.
@@ -2431,6 +2442,7 @@
Verification has been cancelled. You can start verification again.
+ This QR code looks malformed. Please try to verify with another method.
Verification Cancelled
Recovery Passphrase
@@ -3119,6 +3131,8 @@
You don’t have permission to share live location
You need to have the right permissions in order to share live location in this room.
Share location
+
+ Live location
Show Message bubbles
@@ -3172,4 +3186,8 @@
- %d message removed
- %d messages removed
+
+
+ Enable Element Call permission shortcuts
+ Auto-approve Element Call widgets and grant camera / mic access
diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
index 555470e643..80b71a1f75 100644
--- a/vector/src/main/res/xml/vector_settings_labs.xml
+++ b/vector/src/main/res/xml/vector_settings_labs.xml
@@ -77,4 +77,10 @@
android:summary="@string/labs_enable_live_location_summary"
android:title="@string/labs_enable_live_location" />
+
+
diff --git a/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt
new file mode 100644
index 0000000000..fe082ab5b6
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.webkit.PermissionRequest
+import androidx.core.content.ContextCompat.checkSelfPermission
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.amshove.kluent.shouldBe
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class CheckWebViewPermissionsUseCaseTest {
+
+ private val checkWebViewPermissionsUseCase = CheckWebViewPermissionsUseCase()
+
+ private val activity = mockk().apply {
+ every { applicationContext } returns mockk()
+ }
+
+ @Before
+ fun setup() {
+ mockkStatic("androidx.core.content.ContextCompat")
+ }
+
+ @After
+ fun tearDown() {
+ unmockkStatic("androidx.core.content.ContextCompat")
+ }
+
+ @Test
+ fun `given an audio permission is granted when the web client requests audio permission then use case returns true`() {
+ val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE))
+ every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED
+
+ checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true
+ verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL)
+ }
+
+ @Test
+ fun `given a camera permission is granted when the web client requests video permission then use case returns true`() {
+ val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
+ every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED
+
+ checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true
+ verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_VIDEO_IP_CALL)
+ }
+
+ @Test
+ fun `given an audio and camera permissions are granted when the web client requests audio and video permissions then use case returns true`() {
+ val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE))
+ every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED
+
+ checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true
+ verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL + PERMISSIONS_FOR_VIDEO_IP_CALL)
+ }
+
+ @Test
+ fun `given an audio permission is granted but camera isn't when the web client requests audio and video permissions then use case returns false`() {
+ val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE))
+ PERMISSIONS_FOR_AUDIO_IP_CALL.forEach {
+ every { checkSelfPermission(activity.applicationContext, it) } returns PackageManager.PERMISSION_GRANTED
+ }
+ PERMISSIONS_FOR_VIDEO_IP_CALL.forEach {
+ every { checkSelfPermission(activity.applicationContext, it) } returns PackageManager.PERMISSION_DENIED
+ }
+
+ checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false
+ verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL + PERMISSIONS_FOR_VIDEO_IP_CALL.first())
+ }
+
+ @Test
+ fun `given an audio and camera permissions are granted when the web client requests another permission then use case returns false`() {
+ val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_MIDI_SYSEX))
+ every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED
+
+ checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false
+ verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL)
+ }
+
+ private fun verifyPermissionsChecked(context: Context, permissions: List) {
+ permissions.forEach {
+ verify { checkSelfPermission(context, it) }
+ }
+ }
+
+ private fun givenAPermissionRequest(resources: Array): PermissionRequest {
+ return object : PermissionRequest() {
+ override fun getOrigin(): Uri {
+ return mockk()
+ }
+
+ override fun getResources(): Array {
+ return resources
+ }
+
+ override fun grant(resources: Array?) {
+ }
+
+ override fun deny() {
+ }
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index bad37d82cd..a9bbb3eb07 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -102,6 +102,48 @@ class OnboardingViewModelTest {
viewModelWith(initialState)
}
+ @Test
+ fun `given registration started with currentThreePid, when handling InitWith, then emits restored session OnSendEmailSuccess`() = runTest {
+ val test = viewModel.test()
+ fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also {
+ it.givenRegistrationStarted(hasStarted = true)
+ it.givenCurrentThreePid(AN_EMAIL)
+ })
+
+ viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
+
+ test
+ .assertEvents(OnboardingViewEvents.OnSendEmailSuccess(AN_EMAIL, isRestoredSession = true))
+ .finish()
+ }
+
+ @Test
+ fun `given registration not started, when handling InitWith, then does nothing`() = runTest {
+ val test = viewModel.test()
+ fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also { it.givenRegistrationStarted(hasStarted = false) })
+
+ viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
+
+ test
+ .assertNoEvents()
+ .finish()
+ }
+
+ @Test
+ fun `given registration started without currentThreePid, when handling InitWith, then does nothing`() = runTest {
+ val test = viewModel.test()
+ fakeAuthenticationService.givenRegistrationWizard(FakeRegistrationWizard().also {
+ it.givenRegistrationStarted(hasStarted = true)
+ it.givenCurrentThreePid(threePid = null)
+ })
+
+ viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, identityServerUrl = null)))
+
+ test
+ .assertNoEvents()
+ .finish()
+ }
+
@Test
fun `when handling PostViewEvent, then emits contents as view event`() = runTest {
val test = viewModel.test()
@@ -254,6 +296,24 @@ class OnboardingViewModelTest {
.finish()
}
+ @Test
+ fun `given register action returns email success, when handling action, then updates registration state and emits email success`() = runTest {
+ val test = viewModel.test()
+ givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationActionHandler.Result.SendEmailSuccess(AN_EMAIL))
+
+ viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
+
+ test
+ .assertStatesChanges(
+ initialState,
+ { copy(isLoading = true) },
+ { copy(registrationState = RegistrationState(email = AN_EMAIL)) },
+ { copy(isLoading = false) }
+ )
+ .assertEvents(OnboardingViewEvents.OnSendEmailSuccess(AN_EMAIL, isRestoredSession = false))
+ .finish()
+ }
+
@Test
fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
fakeContext.givenHasConnection()
diff --git a/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LockScreenTestMigratorTests.kt b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LockScreenTestMigratorTests.kt
new file mode 100644
index 0000000000..73f71dbf2b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LockScreenTestMigratorTests.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.pin.lockscreen.crypto.migrations
+
+import android.os.Build
+import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
+import im.vector.app.test.TestBuildVersionSdkIntProvider
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+
+class LockScreenTestMigratorTests {
+
+ private val legacyPinCodeMigrator = mockk(relaxed = true)
+ private val missingSystemKeyMigrator = mockk(relaxed = true)
+ private val systemKeyV1Migrator = mockk(relaxed = true)
+ private val versionProvider = TestBuildVersionSdkIntProvider()
+ private val migrator = LockScreenKeysMigrator(legacyPinCodeMigrator, missingSystemKeyMigrator, systemKeyV1Migrator, versionProvider)
+
+ @Test
+ fun `When legacy pin code migration is needed, both legacyPinCodeMigrator and missingSystemKeyMigrator will be run`() {
+ // When no migration is needed
+ every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
+
+ runBlocking { migrator.migrateIfNeeded() }
+
+ coVerify(exactly = 0) { legacyPinCodeMigrator.migrate() }
+ verify(exactly = 0) { missingSystemKeyMigrator.migrate() }
+
+ // When migration is needed
+ every { legacyPinCodeMigrator.isMigrationNeeded() } returns true
+
+ runBlocking { migrator.migrateIfNeeded() }
+
+ coVerify { legacyPinCodeMigrator.migrate() }
+ verify { missingSystemKeyMigrator.migrate() }
+ }
+
+ @Test
+ fun `System key from v1 migration will not be run for versions that don't support biometrics`() {
+ versionProvider.value = Build.VERSION_CODES.LOLLIPOP
+ every { systemKeyV1Migrator.isMigrationNeeded() } returns true
+
+ runBlocking { migrator.migrateIfNeeded() }
+
+ verify(exactly = 0) { systemKeyV1Migrator.migrate() }
+ }
+
+ @Test
+ fun `When system key from v1 migration is needed it will be run`() {
+ versionProvider.value = Build.VERSION_CODES.M
+ every { systemKeyV1Migrator.isMigrationNeeded() } returns false
+
+ runBlocking { migrator.migrateIfNeeded() }
+
+ verify(exactly = 0) { systemKeyV1Migrator.migrate() }
+
+ every { systemKeyV1Migrator.isMigrationNeeded() } returns true
+
+ runBlocking { migrator.migrateIfNeeded() }
+
+ verify { systemKeyV1Migrator.migrate() }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigratorTests.kt b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigratorTests.kt
new file mode 100644
index 0000000000..d65c3da2d2
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigratorTests.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.pin.lockscreen.crypto.migrations
+
+import android.os.Build
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
+import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.test.TestBuildVersionSdkIntProvider
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.amshove.kluent.invoking
+import org.amshove.kluent.shouldNotThrow
+import org.junit.Test
+
+class MissingSystemKeyMigratorTests {
+
+ private val keyStoreCryptoFactory = mockk()
+ private val vectorPreferences = mockk(relaxed = true)
+ private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
+ private val missingSystemKeyMigrator = MissingSystemKeyMigrator("vector.system", keyStoreCryptoFactory, vectorPreferences, versionProvider)
+
+ @Test
+ fun migrateEnsuresSystemKeyExistsIfBiometricAuthIsEnabledAndSupported() {
+ val keyStoreCryptoMock = mockk {
+ every { ensureKey() } returns mockk()
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
+ every { vectorPreferences.useBiometricsToUnlock() } returns true
+
+ missingSystemKeyMigrator.migrate()
+
+ verify { keyStoreCryptoMock.ensureKey() }
+ }
+
+ @Test
+ fun migrateHandlesKeyPermanentlyInvalidatedExceptions() {
+ val keyStoreCryptoMock = mockk {
+ every { ensureKey() } throws KeyPermanentlyInvalidatedException()
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
+ every { vectorPreferences.useBiometricsToUnlock() } returns true
+
+ invoking { missingSystemKeyMigrator.migrate() } shouldNotThrow KeyPermanentlyInvalidatedException::class
+ }
+
+ @Test
+ fun migrateReturnsEarlyIfBiometricAuthIsDisabled() {
+ val keyStoreCryptoMock = mockk {
+ every { ensureKey() } returns mockk()
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
+ every { vectorPreferences.useBiometricsToUnlock() } returns false
+
+ missingSystemKeyMigrator.migrate()
+
+ verify(exactly = 0) { keyStoreCryptoMock.ensureKey() }
+ }
+
+ @Test
+ fun migrateReturnsEarlyIfAndroidVersionCantHandleBiometrics() {
+ versionProvider.value = Build.VERSION_CODES.LOLLIPOP
+ val keyStoreCryptoMock = mockk {
+ every { ensureKey() } returns mockk()
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
+ every { vectorPreferences.useBiometricsToUnlock() } returns false
+
+ missingSystemKeyMigrator.migrate()
+
+ verify(exactly = 0) { keyStoreCryptoMock.ensureKey() }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1MigratorTests.kt b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1MigratorTests.kt
new file mode 100644
index 0000000000..a519251398
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/pin/lockscreen/crypto/migrations/SystemKeyV1MigratorTests.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.pin.lockscreen.crypto.migrations
+
+import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.amshove.kluent.shouldBe
+import org.junit.Test
+import java.security.KeyStore
+
+class SystemKeyV1MigratorTests {
+
+ private val keyStoreCryptoFactory = mockk()
+ private val keyStore = mockk(relaxed = true)
+ private val systemKeyV1Migrator = SystemKeyV1Migrator("vector.system_new", keyStore, keyStoreCryptoFactory)
+
+ @Test
+ fun isMigrationNeededReturnsTrueIfV1KeyExists() {
+ every { keyStore.containsAlias(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) } returns true
+ systemKeyV1Migrator.isMigrationNeeded() shouldBe true
+
+ every { keyStore.containsAlias(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) } returns false
+ systemKeyV1Migrator.isMigrationNeeded() shouldBe false
+ }
+
+ @Test
+ fun migrateDeletesOldEntryAndEnsuresNewKey() {
+ val keyStoreCryptoMock = mockk {
+ every { ensureKey() } returns mockk()
+ }
+ every { keyStoreCryptoFactory.provide("vector.system_new", any()) } returns keyStoreCryptoMock
+
+ systemKeyV1Migrator.migrate()
+
+ verify { keyStore.deleteEntry(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) }
+ verify { keyStoreCryptoMock.ensureKey() }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/pin/lockscreen/fragment/LockScreenViewModelTests.kt b/vector/src/test/java/im/vector/app/features/pin/lockscreen/fragment/LockScreenViewModelTests.kt
index e551b7f563..9e80bb490b 100644
--- a/vector/src/test/java/im/vector/app/features/pin/lockscreen/fragment/LockScreenViewModelTests.kt
+++ b/vector/src/test/java/im/vector/app/features/pin/lockscreen/fragment/LockScreenViewModelTests.kt
@@ -25,6 +25,7 @@ import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
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.LockScreenKeysMigrator
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
import im.vector.app.features.pin.lockscreen.ui.AuthMethod
import im.vector.app.features.pin.lockscreen.ui.LockScreenAction
@@ -56,7 +57,8 @@ class LockScreenViewModelTests {
private val pinCodeHelper = mockk(relaxed = true)
private val biometricHelper = mockk(relaxed = true)
- private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
+ private val keysMigrator = mockk(relaxed = true)
+ private val versionProvider = TestBuildVersionSdkIntProvider()
@Before
fun setup() {
@@ -67,9 +69,9 @@ class LockScreenViewModelTests {
fun `init migrates old keys to new ones if needed`() {
val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
- LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
- coVerify { pinCodeHelper.migratePinCodeIfNeeded() }
+ coVerify { keysMigrator.migrateIfNeeded() }
}
@Test
@@ -78,7 +80,7 @@ class LockScreenViewModelTests {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
// This should set canUseBiometricAuth to true
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val newState = withState(viewModel) { it }
initialState shouldNotBeEqualTo newState
}
@@ -87,7 +89,7 @@ class LockScreenViewModelTests {
fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest {
val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coEvery { pinCodeHelper.verifyPinCode(any()) } returns true
val events = viewModel.test().viewEvents
@@ -112,7 +114,7 @@ class LockScreenViewModelTests {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false)
val initialState = createViewState(lockScreenConfiguration = configuration)
val configProvider = LockScreenConfiguratorProvider(configuration)
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents
events.assertNoValues()
@@ -128,7 +130,7 @@ class LockScreenViewModelTests {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val configProvider = LockScreenConfiguratorProvider(configuration)
val initialState = createViewState(lockScreenConfiguration = configuration)
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents
events.assertNoValues()
@@ -148,7 +150,7 @@ class LockScreenViewModelTests {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val initialState = createViewState(lockScreenConfiguration = configuration)
val configProvider = LockScreenConfiguratorProvider(configuration)
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents
events.assertNoValues()
@@ -169,7 +171,7 @@ class LockScreenViewModelTests {
fun `onPinCodeEntered handles exceptions`() = runTest {
val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val exception = IllegalStateException("Something went wrong")
coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception
@@ -183,7 +185,7 @@ class LockScreenViewModelTests {
@Test
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
- buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+ versionProvider.value = Build.VERSION_CODES.M
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
every { biometricHelper.isSystemKeyValid } returns true
@@ -199,7 +201,7 @@ class LockScreenViewModelTests {
isBiometricKeyInvalidated = false,
lockScreenConfiguration = configuration
)
- val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents
events.assertNoValues()
@@ -217,7 +219,7 @@ class LockScreenViewModelTests {
@Test
fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
- val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coEvery { biometricHelper.authenticate(any()) } returns flowOf(false, true)
val events = viewModel.test().viewEvents
@@ -231,7 +233,7 @@ class LockScreenViewModelTests {
@Test
fun `showBiometricPrompt handles exceptions`() = runTest {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
- val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider)
+ val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val exception = IllegalStateException("Something went wrong")
coEvery { biometricHelper.authenticate(any()) } throws exception
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
index e0b4586931..4f0b1fe083 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
@@ -45,6 +45,14 @@ class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
}
}
+ fun givenRegistrationStarted(hasStarted: Boolean) {
+ coEvery { isRegistrationStarted() } returns hasStarted
+ }
+
+ fun givenCurrentThreePid(threePid: String?) {
+ coEvery { getCurrentThreePid() } returns threePid
+ }
+
fun givenUserNameIsAvailable(userName: String) {
coEvery { registrationAvailable(userName) } returns RegistrationAvailability.Available
}