1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Merge branch 'develop' into feature/performance_tracking

This commit is contained in:
ganfra
2020-10-01 17:29:22 +02:00
committed by GitHub
85 changed files with 2723 additions and 657 deletions

View File

@@ -0,0 +1,18 @@
name: Update Gradle Wrapper
on:
schedule:
- cron: "0 0 * * *"
jobs:
update-gradle-wrapper:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
target-branch: develop

View File

@@ -1,9 +1,36 @@
Changes in Element 1.0.8 (2020-XX-XX)
Changes in Element 1.0.9 (2020-XX-XX)
===================================================
Features ✨:
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
Improvements 🙌:
- Wording differentiation for direct rooms (#2176)
- PIN code: request PIN code if phone has been locked
- Small optimisation of scrolling experience in timeline (#2114)
- Allow user to reset cross signing if he has no way to recover (#2052)
Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376)
- Fix Splash layout on small screens
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
- Use Update Gradle Wrapper Action
- Updates Gradle Wrapper from 5.6.4 to 6.6.1. (#2193)
Other changes:
- Added registration/verification automated UI tests
- Create a script to help getting public information form any homeserver
Changes in Element 1.0.8 (2020-09-25)
===================================================
Improvements 🙌:
- Add "show password" in import Megolm keys dialog
- Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112)
@@ -15,22 +42,17 @@ Improvements 🙌:
- PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985)
Bugfix 🐛:
- Long message cannot be sent/takes infinite time & blocks other messages #1397
- Long message cannot be sent/takes infinite time & blocks other messages (#1397)
- Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes)
- User Verification in DM not working
- Manual import of Megolm keys does back up the imported keys
- Auto scrolling to the latest message when sending (#2094)
- Fix incorrect permission check when creating widgets (#2137)
- Pin code: user has to enter pin code twice (#2005)
Translations 🗣:
-
SDK API changes ⚠️:
- Rename `tryThis` to `tryOrNull`
Build 🧱:
-
Other changes:
- Add an advanced action to reset an account data entry

107
docs/ui-tests.md Normal file
View File

@@ -0,0 +1,107 @@
# Automate user interface tests
Element Android ensures that some fundamental flows are properly working by running automated user interface tests.
Ui tests are using the android [Espresso](https://developer.android.com/training/testing/espresso) library.
Tests can be run on a real device, or on a virtual device (such as the emulator in Android Studio).
Currently the test are covering a small set of application flows:
- Registration
- Self verification via emoji
- Self verification via passphrase
## Prerequisites:
Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses).
You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are:
```shell script
$ git clone https://github.com/matrix-org/synapse.git
$ cd synapse
$ virtualenv -p python3 env
$ source env/bin/activate
(env) $ python -m pip install --no-use-pep517 -e .
```
Every time you want to launch these test homeservers, type:
```shell script
$ virtualenv -p python3 env
$ source env/bin/activate
(env) $ demo/start.sh --no-rate-limit
```
**Emulator/Device set up**
When running the test via android studio on a device, you have to disable system animations in order for the test to work properly.
First, ensure developer mode is enabled:
- To enable developer options, tap the **Build Number** option 7 times. You can find this option in one of the following locations, depending on your Android version:
- Android 9 (API level 28) and higher: **Settings > About Phone > Build Number**
- Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): **Settings > System > About Phone > Build Number**
- Android 7.1 (API level 25) and lower: **Settings > About Phone > Build Number**
On your device, under **Settings > Developer options**, disable the following 3 settings:
- Window animation scale
- Transition animation scale
- Animator duration scale
## Run the tests
Once Synapse is running, and an emulator is running, you can run the UI tests.
### From the source code
Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue).
### From command line
````shell script
./gradlew vector:connectedGplayDebugAndroidTest
````
To run all the tests from the `vector` module.
In case of trouble, you can try to uninstall the previous installed test APK first with this command:
```shell script
adb uninstall im.vector.app.debug.test
```
## Recipes
We added some specific Espresso IdlingResources, and other utilities for matrix related tests
### Wait for initial sync
```kotlin
// Wait for initial sync and check room list is there
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
```
### Accessing current activity
```kotlin
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
```
### Interact with other session
It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example)
```kotlin
@Before
fun initAccount() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
}
```

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Thu Jul 02 12:33:07 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

53
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -218,7 +218,7 @@ class CommonTestHelper(context: Context) {
.createAccount(userName, password, null, it)
}
// Preform dummy step
// Perform dummy step
val registrationResult = doSync<RegistrationResult> {
matrix.authenticationService
.getRegistrationWizard()

View File

@@ -126,7 +126,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request
*/
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,

View File

@@ -372,6 +372,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk
@@ -407,6 +408,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeMSKPrivateKey(msk: String?) {
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk
@@ -415,6 +417,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeSSKPrivateKey(ssk: String?) {
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignSelfSignedPrivateKey = ssk
@@ -423,6 +426,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeUSKPrivateKey(usk: String?) {
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = usk

View File

@@ -10,13 +10,19 @@
<string name="notice_room_invite_no_invitee_by_you">Your invitation</string>
<string name="notice_room_created">%1$s created the room</string>
<string name="notice_room_created_by_you">You created the room</string>
<string name="notice_direct_room_created">%1$s created the discussion</string>
<string name="notice_direct_room_created_by_you">You created the discussion</string>
<string name="notice_room_invite">%1$s invited %2$s</string>
<string name="notice_room_invite_by_you">You invited %1$s</string>
<string name="notice_room_invite_you">%1$s invited you</string>
<string name="notice_room_join">%1$s joined the room</string>
<string name="notice_room_join_by_you">You joined the room</string>
<string name="notice_direct_room_join">%1$s joined</string>
<string name="notice_direct_room_join_by_you">You joined</string>
<string name="notice_room_leave">%1$s left the room</string>
<string name="notice_room_leave_by_you">You left the room</string>
<string name="notice_direct_room_leave">%1$s left the room</string>
<string name="notice_direct_room_leave_by_you">You left the room</string>
<string name="notice_room_reject">%1$s rejected the invitation</string>
<string name="notice_room_reject_by_you">You rejected the invitation</string>
<string name="notice_room_kick">%1$s kicked %2$s</string>
@@ -53,6 +59,8 @@
<string name="notice_ended_call_by_you">You ended the call.</string>
<string name="notice_made_future_room_visibility">%1$s made future room history visible to %2$s</string>
<string name="notice_made_future_room_visibility_by_you">You made future room history visible to %1$s</string>
<string name="notice_made_future_direct_room_visibility">%1$s made future messages visible to %2$s</string>
<string name="notice_made_future_direct_room_visibility_by_you">You made future messages visible to %1$s</string>
<string name="notice_room_visibility_invited">all room members, from the point they are invited.</string>
<string name="notice_room_visibility_joined">all room members, from the point they joined.</string>
<string name="notice_room_visibility_shared">all room members.</string>
@@ -62,6 +70,8 @@
<string name="notice_end_to_end_by_you">You turned on end-to-end encryption (%1$s)</string>
<string name="notice_room_update">%s upgraded this room.</string>
<string name="notice_room_update_by_you">You upgraded this room.</string>
<string name="notice_direct_room_update">%s upgraded here.</string>
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
<string name="notice_requested_voip_conference">%1$s requested a VoIP conference</string>
<string name="notice_requested_voip_conference_by_you">You requested a VoIP conference</string>
@@ -83,8 +93,12 @@
<string name="notice_profile_change_redacted_by_you">You updated your profile %1$s</string>
<string name="notice_room_third_party_invite">%1$s sent an invitation to %2$s to join the room</string>
<string name="notice_room_third_party_invite_by_you">You sent an invitation to %1$s to join the room</string>
<string name="notice_direct_room_third_party_invite">%1$s invited %2$s</string>
<string name="notice_direct_room_third_party_invite_by_you">You invited %1$s</string>
<string name="notice_room_third_party_revoked_invite">%1$s revoked the invitation for %2$s to join the room</string>
<string name="notice_room_third_party_revoked_invite_by_you">You revoked the invitation for %1$s to join the room</string>
<string name="notice_direct_room_third_party_revoked_invite">%1$s revoked the invitation for %2$s</string>
<string name="notice_direct_room_third_party_revoked_invite_by_you">You revoked the invitation for %1$s</string>
<string name="notice_room_third_party_registered_invite">%1$s accepted the invitation for %2$s</string>
<string name="notice_room_third_party_registered_invite_by_you">You accepted the invitation for %1$s</string>
@@ -171,8 +185,12 @@
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined the room. Reason: %2$s</string>
<string name="notice_room_join_with_reason_by_you">You joined the room. Reason: %1$s</string>
<string name="notice_direct_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_direct_room_join_with_reason_by_you">You joined. Reason: %1$s</string>
<string name="notice_room_leave_with_reason">%1$s left the room. Reason: %2$s</string>
<string name="notice_room_leave_with_reason_by_you">You left the room. Reason: %1$s</string>
<string name="notice_direct_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_direct_room_leave_with_reason_by_you">You left. Reason: %1$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_reject_with_reason_by_you">You rejected the invitation. Reason: %1$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
@@ -220,8 +238,12 @@
<string name="notice_room_guest_access_can_join">"%1$s has allowed guests to join the room."</string>
<string name="notice_room_guest_access_can_join_by_you">"You have allowed guests to join the room."</string>
<string name="notice_direct_room_guest_access_can_join">"%1$s has allowed guests to join here."</string>
<string name="notice_direct_room_guest_access_can_join_by_you">"You have allowed guests to join here."</string>
<string name="notice_room_guest_access_forbidden">"%1$s has prevented guests from joining the room."</string>
<string name="notice_room_guest_access_forbidden_by_you">"You have prevented guests from joining the room."</string>
<string name="notice_direct_room_guest_access_forbidden">"%1$s has prevented guests from joining the room."</string>
<string name="notice_direct_room_guest_access_forbidden_by_you">"You have prevented guests from joining the room."</string>
<string name="notice_end_to_end_ok">%1$s turned on end-to-end encryption.</string>
<string name="notice_end_to_end_ok_by_you">You turned on end-to-end encryption.</string>

View File

@@ -84,7 +84,7 @@ class AudioPicker(override val requestCode: Int) : Picker<MultiPickerAudioType>(
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "audio/*"

View File

@@ -64,7 +64,7 @@ class FilePicker(override val requestCode: Int) : Picker<MultiPickerFileType>(re
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "*/*"

View File

@@ -82,7 +82,7 @@ class ImagePicker(override val requestCode: Int) : Picker<MultiPickerImageType>(
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "image/*"

View File

@@ -93,7 +93,7 @@ class VideoPicker(override val requestCode: Int) : Picker<MultiPickerVideoType>(
}
override fun createIntent(): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "video/*"

View File

@@ -32,8 +32,6 @@
import static
### Rubbish from merge. Please delete those lines (sometimes in comment)
<<<<<<<
>>>>>>>
### carry return before "}". Please remove empty lines.
\n\s*\n\s*\}
@@ -164,7 +162,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
enum class===79
enum class===81
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

69
tools/hs_diag.py Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# Copyright (c) 2020 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.
import argparse
import os
### Arguments
parser = argparse.ArgumentParser(description='Get some information about a homeserver.')
parser.add_argument('-s',
'--homeserver',
required=True,
help="homeserver URL")
parser.add_argument('-v',
'--verbose',
help="increase output verbosity.",
action="store_true")
args = parser.parse_args()
if args.verbose:
print("Argument:")
print(args)
baseUrl = args.homeserver
if not baseUrl.startswith("http"):
baseUrl = "https://" + baseUrl
if not baseUrl.endswith("/"):
baseUrl = baseUrl + "/"
print("Get information from " + baseUrl)
items = [
# [Title, URL, True for GET request and False for POST request]
["Well-known", baseUrl + ".well-known/matrix/client", True]
, ["Version", baseUrl + "_matrix/client/versions", True]
, ["Login flow", baseUrl + "_matrix/client/r0/login", True]
, ["Registration flow", baseUrl + "_matrix/client/r0/register", False]
# Useless , ["Username availability", baseUrl + "_matrix/client/r0/register/available?username=benoit", True]
# Useless , ["Public rooms", baseUrl + "_matrix/client/r0/publicRooms?limit=1", True]
# Useless , ["Profile", baseUrl + "_matrix/client/r0/profile/@benoit.marty:matrix.org", True]
# Need token , ["Capability", baseUrl + "_matrix/client/r0/capabilities", True]
# Need token , ["Media config", baseUrl + "_matrix/media/r0/config", True]
# Need token , ["Turn", baseUrl + "_matrix/client/r0/voip/turnServer", True]
]
for item in items:
print("====================================================================================================")
print("# " + item[0] + " (" + item[1] + ")")
print("====================================================================================================")
if item[2]:
os.system("curl -s -X GET '" + item[1] + "' | python -m json.tool")
else:
os.system("curl -s -X POST --data $'{}' '" + item[1] + "' | python -m json.tool")

View File

@@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 0
ext.versionPatch = 8
ext.versionPatch = 9
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -172,6 +172,19 @@ android {
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
}
}
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
// Disables animations during instrumented tests you run from the command line…
// This property does not affect tests that you run using Android Studio.”
animationsDisabled = true
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
signingConfigs {
@@ -281,6 +294,11 @@ dependencies {
def arch_version = '2.1.0'
def lifecycle_version = '2.2.0'
// Tests
def kluent_version = '1.44'
def androidxTest_version = '1.3.0'
def espresso_version = '3.3.0'
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch")
@@ -329,6 +347,7 @@ dependencies {
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
implementation("com.airbnb.android:epoxy:$epoxy_version")
implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version"
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
implementation 'com.airbnb.android:mvrx:1.3.0'
@@ -424,19 +443,21 @@ dependencies {
// TESTS
testImplementation 'junit:junit:4.12'
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
// Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// Activate when you want to check for leaks, from time to time.
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "androidx.test:core:$androidxTest_version"
androidTestImplementation "androidx.test:runner:$androidxTest_version"
androidTestImplementation "androidx.test:rules:$androidxTest_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version"
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
// Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'

View File

@@ -0,0 +1,207 @@
/*
* Copyright (c) 2020 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.app.Activity
import android.view.View
import androidx.lifecycle.Observer
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.StringDescription
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import java.util.concurrent.TimeoutException
object EspressoHelper {
fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null
getInstrumentation().runOnMainSync {
currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
}
return currentActivity
}
}
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.any(View::class.java)
}
override fun getDescription(): String {
val matcherDescription = StringDescription()
viewMatcher.describeTo(matcherDescription)
return "wait for a specific view <$matcherDescription> to be ${if (waitForDisplayed) "displayed" else "not displayed during $timeout millis."}"
}
override fun perform(uiController: UiController, view: View) {
println("*** waitForView 1 $view")
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + timeout
val visibleMatcher = isDisplayed()
do {
println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}")
val viewVisible = TreeIterables.breadthFirstViewTraversal(view)
.any { viewMatcher.matches(it) && visibleMatcher.matches(it) }
println("*** waitForView loop viewVisible:$viewVisible")
if (viewVisible == waitForDisplayed) return
println("*** waitForView loop loopMainThreadForAtLeast...")
uiController.loopMainThreadForAtLeast(50)
println("*** waitForView loop ...loopMainThreadForAtLeast")
} while (System.currentTimeMillis() < endTime)
println("*** waitForView timeout $view")
// Timeout happens.
throw PerformException.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))
.withCause(TimeoutException())
.build()
}
}
}
fun initialSyncIdlingResource(session: Session): IdlingResource {
val res = object : IdlingResource, Observer<SyncState> {
private var callback: IdlingResource.ResourceCallback? = null
override fun getName() = "InitialSyncIdlingResource for ${session.myUserId}"
override fun isIdleNow(): Boolean {
val isIdle = session.hasAlreadySynced()
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
override fun onChanged(t: SyncState?) {
val isIdle = session.hasAlreadySynced()
if (isIdle) {
callback?.onTransitionToIdle()
session.getSyncStateLive().removeObserver(this)
}
}
}
runOnUiThread {
session.getSyncStateLive().observeForever(res)
}
return res
}
fun activityIdlingResource(activityClass: Class<*>): IdlingResource {
val res = object : IdlingResource, ActivityLifecycleCallback {
private var callback: IdlingResource.ResourceCallback? = null
var hasResumed = false
private var currentActivity : Activity? = null
val uniqTS = System.currentTimeMillis()
override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS"
override fun isIdleNow(): Boolean {
val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle")
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
println("*** [$name] registerIdleTransitionCallback $callback")
this.callback = callback
// if (hasResumed) callback?.onTransitionToIdle()
}
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) {
println("*** [$name] onActivityLifecycleChanged $activity $stage")
currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle")
if (isIdle) {
hasResumed = true
println("*** [$name] onActivityLifecycleChanged callback: $callback")
callback?.onTransitionToIdle()
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this)
}
}
}
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(res)
return res
}
fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) {
println("*** withIdlingResource register")
IdlingRegistry.getInstance().register(idlingResource)
block.invoke()
println("*** withIdlingResource unregister")
IdlingRegistry.getInstance().unregister(idlingResource)
}
fun allSecretsKnownIdling(session: Session): IdlingResource {
val res = object : IdlingResource, Observer<Optional<PrivateKeysInfo>> {
private var callback: IdlingResource.ResourceCallback? = null
var privateKeysInfo: PrivateKeysInfo? = session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()
override fun getName() = "AllSecretsKnownIdling_${session.myUserId}"
override fun isIdleNow(): Boolean {
println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}")
return privateKeysInfo?.allKnown() == true
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
override fun onChanged(t: Optional<PrivateKeysInfo>?) {
println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}")
privateKeysInfo = t?.getOrNull()
if (t?.getOrNull()?.allKnown() == true) {
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this)
callback?.onTransitionToIdle()
}
}
}
runOnUiThread {
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().observeForever(res)
}
return res
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (c) 2020 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 androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
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.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class RegistrationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun simpleRegister() {
val userId: String = "UiAutoTest_${System.currentTimeMillis()}"
val password: String = "password"
val homeServerUrl: String = "http://10.0.2.2:8080"
// Check splashscreen is there
onView(withId(R.id.loginSplashSubmit))
.check(matches(isDisplayed()))
.check(matches(withText(R.string.login_splash_submit)))
// Click on get started
onView(withId(R.id.loginSplashSubmit))
.perform(click())
// Check that home server options are shown
onView(withId(R.id.loginServerTitle))
.check(matches(isDisplayed()))
.check(matches(withText(R.string.login_server_title)))
// Chose custom server
onView(withId(R.id.loginServerChoiceOther))
.perform(click())
// Enter local synapse
onView((withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(typeText(homeServerUrl))
// Click on continue
onView(withId(R.id.loginServerUrlFormSubmit))
.check(matches(isEnabled()))
.perform(closeSoftKeyboard(), click())
// Click on the signup button
onView(withId(R.id.loginSignupSigninSubmit))
.check(matches(isDisplayed()))
.perform(click())
// Ensure password flow supported
onView(withId(R.id.loginField))
.check(matches(isDisplayed()))
onView(withId(R.id.passwordField))
.check(matches(isDisplayed()))
// Ensure user id
onView((withId(R.id.loginField)))
.perform(typeText(userId))
// Ensure login button not yet enabled
onView(withId(R.id.loginSubmit))
.check(matches(not(isEnabled())))
// Ensure password
onView((withId(R.id.passwordField)))
.perform(closeSoftKeyboard(), typeText(password))
// Submit
onView(withId(R.id.loginSubmit))
.check(matches(isEnabled()))
.perform(closeSoftKeyboard(), click())
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
// Wait for initial sync and check room list is there
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright (c) 2020 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.app.Activity
import android.app.Instrumentation.ActivityResult
import android.content.Intent
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.MainActivity
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.session.Session
@RunWith(AndroidJUnit4::class)
@LargeTest
class SecurityBootstrapTest : VerificationTestBase() {
var existingSession: Session? = null
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun createSessionWithCrossSigning() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
stubAllExternalIntents()
}
private fun stubAllExternalIntents() {
// By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before
// every test run. In this case all external Intents will be blocked.
Intents.init()
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
}
@Test
fun testBasicBootstrap() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
.perform(closeSoftKeyboard())
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
activity.navigator.open4SSetup(activity, SetupMode.NORMAL)
Thread.sleep(1000)
onView(withId(R.id.bootstrapSetupSecureUseSecurityKey))
.check(matches(isDisplayed()))
onView(withId(R.id.bootstrapSetupSecureUseSecurityPassphrase))
.check(matches(isDisplayed()))
.perform(click())
onView(isRoot())
.perform(waitForView(withText(R.string.bootstrap_info_text_2)))
// test back
onView(isRoot()).perform(pressBack())
Thread.sleep(1000)
onView(withId(R.id.bootstrapSetupSecureUseSecurityKey))
.check(matches(isDisplayed()))
onView(withId(R.id.bootstrapSetupSecureUseSecurityPassphrase))
.check(matches(isDisplayed()))
.perform(click())
onView(isRoot())
.perform(waitForView(withText(R.string.bootstrap_info_text_2)))
onView(withId(R.id.ssss_passphrase_enter_edittext))
.perform(typeText("person woman man camera tv"))
onView(withId(R.id.bootstrapSubmit))
.perform(closeSoftKeyboard(), click())
// test bad pass
onView(withId(R.id.ssss_passphrase_enter_edittext))
.perform(typeText("person woman man cmera tv"))
onView(withId(R.id.bootstrapSubmit))
.perform(closeSoftKeyboard(), click())
onView(withText(R.string.passphrase_passphrase_does_not_match)).check(matches(isDisplayed()))
onView(withId(R.id.ssss_passphrase_enter_edittext))
.perform(replaceText("person woman man camera tv"))
onView(withId(R.id.bootstrapSubmit))
.perform(closeSoftKeyboard(), click())
onView(withId(R.id.bottomSheetScrollView))
.perform(waitForView(withText(R.string.bottom_sheet_save_your_recovery_key_content)))
intending(hasAction(Intent.ACTION_SEND)).respondWith(ActivityResult(Activity.RESULT_OK, null))
onView(withId(R.id.recoveryCopy))
.perform(click())
Thread.sleep(1000)
// Dismiss dialog
onView(withText(R.string.ok)).inRoot(RootMatchers.isDialog()).perform(click())
onView(withId(R.id.bottomSheetScrollView))
.perform(waitForView(withText(R.string.bottom_sheet_save_your_recovery_key_content)))
onView(withText(R.string._continue)).perform(click())
// Assert that all is configured
assert(uiSession.cryptoService().crossSigningService().isCrossSigningInitialized())
assert(uiSession.cryptoService().crossSigningService().canCrossSign())
assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown())
assert(uiSession.cryptoService().keysBackupService().isEnabled)
assert(uiSession.cryptoService().keysBackupService().currentBackupVersion != null)
assert(uiSession.sharedSecretStorageService.isRecoverySetup())
assert(uiSession.sharedSecretStorageService.isMegolmKeyInBackup())
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 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.UiController;
import androidx.test.espresso.ViewAction;
import org.hamcrest.Matcher;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
public class SleepViewAction {
public static ViewAction sleep(final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "Wait for at least " + millis + " millis";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
uiController.loopMainThreadForAtLeast(millis);
}
};
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 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 androidx.annotation.CallSuper
import junit.framework.TestCase.fail
import org.matrix.android.sdk.api.MatrixCallback
import timber.log.Timber
import java.util.concurrent.CountDownLatch
/**
* Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback
* @param onlySuccessful true to fail if an error occurs. This is the default behavior
* @param <T>
*/
open class TestMatrixCallback<T>(private val countDownLatch: CountDownLatch,
private val onlySuccessful: Boolean = true) : MatrixCallback<T> {
@CallSuper
override fun onSuccess(data: T) {
countDownLatch.countDown()
}
@CallSuper
override fun onFailure(failure: Throwable) {
Timber.e(failure, "TestApiCallback")
if (onlySuccessful) {
fail("onFailure " + failure.localizedMessage)
}
countDownLatch.countDown()
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright (c) 2020 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.net.Uri
import androidx.lifecycle.Observer
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.junit.Assert
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.SyncState
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
abstract class VerificationTestBase {
val password = "password"
val homeServerUrl: String = "http://10.0.2.2:8080"
fun doLogin(homeServerUrl: String, userId: String, password: String) {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signin button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
}
private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signup button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
fun createAccountAndSync(matrix: Matrix, userName: String,
password: String,
withInitialSync: Boolean): Session {
val hs = createHomeServerConfig()
doSync<LoginFlowResult> {
matrix.authenticationService()
.getLoginFlow(hs, it)
}
doSync<RegistrationResult> {
matrix.authenticationService()
.getRegistrationWizard()
.createAccount(userName, password, null, it)
}
// Perform dummy step
val registrationResult = doSync<RegistrationResult> {
matrix.authenticationService()
.getRegistrationWizard()
.dummy(it)
}
Assert.assertTrue(registrationResult is RegistrationResult.Success)
val session = (registrationResult as RegistrationResult.Success).session
if (withInitialSync) {
syncSession(session)
}
return session
}
fun createHomeServerConfig(): HomeServerConnectionConfig {
return HomeServerConnectionConfig.Builder()
.withHomeServerUri(Uri.parse(homeServerUrl))
.build()
}
// Transform a method with a MatrixCallback to a synchronous method
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
val lock = CountDownLatch(1)
var result: T? = null
val callback = object : TestMatrixCallback<T>(lock) {
override fun onSuccess(data: T) {
result = data
super.onSuccess(data)
}
}
block.invoke(callback)
lock.await(20_000, TimeUnit.MILLISECONDS)
Assert.assertNotNull(result)
return result!!
}
fun syncSession(session: Session) {
val lock = CountDownLatch(1)
GlobalScope.launch(Dispatchers.Main) { session.open() }
session.startSync(true)
val syncLiveData = runBlocking(Dispatchers.Main) {
session.getSyncStateLive()
}
val syncObserver = object : Observer<SyncState> {
override fun onChanged(t: SyncState?) {
if (session.hasAlreadySynced()) {
lock.countDown()
syncLiveData.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
lock.await(20_000, TimeUnit.MILLISECONDS)
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright (c) 2020 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.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@RunWith(AndroidJUnit4::class)
@LargeTest
class VerifySessionInteractiveTest : VerificationTestBase() {
var existingSession: Session? = null
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun createSessionWithCrossSigning() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password"
), it)
}
}
@Test
fun checkVerifyPopup() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
.perform(closeSoftKeyboard())
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
onView(isRoot())
.perform(waitForView(withId(com.tapadoo.alerter.R.id.llAlertBackground)))
// Thread.sleep(1000)
// onView(withId(com.tapadoo.alerter.R.id.llAlertBackground))
// .perform(click())
Thread.sleep(1000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
onView(isRoot())
.perform(waitForView(withId(R.id.bottomSheetFragmentContainer)))
// .check()
// onView(withId(R.id.bottomSheetFragmentContainer))
// .check(matches(isDisplayed()))
// onView(isRoot()).perform(SleepViewAction.sleep(2000))
onView(withText(R.string.use_latest_app))
.check(matches(isDisplayed()))
// 4S is not setup so passphrase option should be hidden
onView(withId(R.id.bottomSheetFragmentContainer))
.check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session)))))
val request = existingSession!!.cryptoService().verificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
existingSession!!.myUserId,
listOf(uiSession.sessionParams.deviceId!!)
)
val transactionId = request.transactionId!!
val sasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, uiSession)
val otherSessionSasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, existingSession!!)
onView(isRoot()).perform(SleepViewAction.sleep(1000))
// Assert QR code option is there and available
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withText(R.string.verification_scan_their_code))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withId(R.id.itemVerificationQrCodeImage))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.verification_scan_emoji_title)),
click()
)
)
val firstSessionTr = existingSession!!.cryptoService().verificationService().getExistingTransaction(
existingSession!!.myUserId,
transactionId
) as SasVerificationTransaction
IdlingRegistry.getInstance().register(sasReadyIdle)
IdlingRegistry.getInstance().register(otherSessionSasReadyIdle)
onView(isRoot()).perform(SleepViewAction.sleep(300))
// will only execute when Idle is ready
val expectedEmojis = firstSessionTr.getEmojiCodeRepresentation()
val targets = listOf(R.id.emoji0, R.id.emoji1, R.id.emoji2, R.id.emoji3, R.id.emoji4, R.id.emoji5, R.id.emoji6)
targets.forEachIndexed { index, res ->
onView(withId(res))
.check(
matches(hasDescendant(withText(expectedEmojis[index].nameResId)))
)
}
IdlingRegistry.getInstance().unregister(sasReadyIdle)
IdlingRegistry.getInstance().unregister(otherSessionSasReadyIdle)
val verificationSuccessIdle =
verificationStateIdleResource(transactionId, VerificationTxState.Verified, uiSession)
// CLICK ON THEY MATCH
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.verification_sas_match)),
click()
)
)
firstSessionTr.userHasVerifiedShortCode()
onView(isRoot()).perform(SleepViewAction.sleep(1000))
withIdlingResource(verificationSuccessIdle) {
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
)
}
// Wait a bit before done (to delay a bit sending of secrets to let other have time
// to mark as verified :/
Thread.sleep(5_000)
// Click on done
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.done)),
click()
)
)
// Wait until local secrets are known (gossip)
withIdlingResource(allSecretsKnownIdling(uiSession)) {
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
}
}
fun signout() {
onView((withId(R.id.groupToolbarAvatarImageView)))
.perform(click())
onView((withId(R.id.homeDrawerHeaderSettingsView)))
.perform(click())
onView(withText("General"))
.perform(click())
}
fun verificationStateIdleResource(transactionId: String, checkForState: VerificationTxState, session: Session): IdlingResource {
val idle = object : IdlingResource, VerificationService.Listener {
private var callback: IdlingResource.ResourceCallback? = null
private var currentState: VerificationTxState? = null
override fun getName() = "verificationSuccessIdle"
override fun isIdleNow(): Boolean {
return currentState == checkForState
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
fun update(state: VerificationTxState) {
currentState = state
if (state == checkForState) {
session.cryptoService().verificationService().removeListener(this)
callback?.onTransitionToIdle()
}
}
/**
* Called when a transaction is created, either by the user or initiated by the other user.
*/
override fun transactionCreated(tx: VerificationTransaction) {
if (tx.transactionId == transactionId) update(tx.state)
}
/**
* Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction.
*/
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.transactionId == transactionId) update(tx.state)
}
}
session.cryptoService().verificationService().addListener(idle)
return idle
}
object UITestVerificationUtils
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (c) 2020 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.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.MainActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
import im.vector.app.features.crypto.recover.Params
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.home.HomeActivity
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@RunWith(AndroidJUnit4::class)
@LargeTest
class VerifySessionPassphraseTest : VerificationTestBase() {
var existingSession: Session? = null
val passphrase = "person woman camera tv"
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun createSessionWithCrossSigningAnd4S() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password"
), it)
}
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
runBlocking {
task.execute(Params(
userPasswordAuth = UserPasswordAuth(password = password),
passphrase = passphrase,
setupMode = SetupMode.NORMAL
))
}
}
@Test
fun checkVerifyWithPassphrase() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
.perform(closeSoftKeyboard())
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
Thread.sleep(6000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
onView(withId(R.id.bottomSheetFragmentContainer))
.check(matches(isDisplayed()))
onView(isRoot()).perform(SleepViewAction.sleep(2000))
onView(withText(R.string.use_latest_app))
.check(matches(isDisplayed()))
// 4S is not setup so passphrase option should be hidden
onView(withId(R.id.bottomSheetFragmentContainer))
.check(matches(hasDescendant(withText(R.string.verification_cannot_access_other_session))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.verification_cannot_access_other_session)),
click()
)
)
withIdlingResource(activityIdlingResource(SharedSecureStorageActivity::class.java)) {
onView(withId(R.id.ssss__root)).check(matches(isDisplayed()))
}
onView((withId(R.id.ssss_passphrase_enter_edittext)))
.perform(typeText(passphrase))
onView((withId(R.id.ssss_passphrase_submit)))
.perform(click())
System.out.println("*** passphrase 1")
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
System.out.println("*** passphrase 1.1")
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
)
}
System.out.println("*** passphrase 2")
// check that all secrets are known?
assert(uiSession.cryptoService().crossSigningService().canCrossSign())
assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown())
Thread.sleep(10_000)
}
}

View File

@@ -17,7 +17,10 @@
package im.vector.app
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Handler
import android.os.HandlerThread
@@ -92,6 +95,15 @@ class VectorApplication :
// font thread handler
private var fontThreadHandler: Handler? = null
private val powerKeyReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == Intent.ACTION_SCREEN_OFF
&& vectorPreferences.useFlagPinCode()) {
pinLocker.screenIsOff()
}
}
}
override fun onCreate() {
enableStrictModeIfNeeded()
super.onCreate()
@@ -163,6 +175,12 @@ class VectorApplication :
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
// This should be done as early as possible
// initKnownEmojiHashSet(appContext)
applicationContext.registerReceiver(powerKeyReceiver, IntentFilter().apply {
// Looks like i cannot receive OFF, if i don't have both ON and OFF
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
})
}
private fun enableStrictModeIfNeeded() {

View File

@@ -27,6 +27,7 @@ import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
@@ -530,6 +531,11 @@ interface FragmentModule {
@FragmentKey(SharedSecuredStorageKeyFragment::class)
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SharedSecuredStorageResetAllFragment::class)
fun bindSharedSecuredStorageResetAllFragment(fragment: SharedSecuredStorageResetAllFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SetIdentityServerFragment::class)

View File

@@ -17,6 +17,7 @@ package im.vector.app.core.platform
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -86,6 +87,24 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
open val showExpanded = false
interface ResultListener {
fun onBottomSheetResult(resultCode: Int, data: Any?)
companion object {
const val RESULT_OK = 1
const val RESULT_CANCEL = 0
}
}
var resultListener : ResultListener? = null
var bottomSheetResult: Int = ResultListener.RESULT_CANCEL
var bottomSheetResultData: Any? = null
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
resultListener?.onBottomSheetResult(bottomSheetResult, bottomSheetResultData)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(getLayoutResId(), container, false)
unBinder = ButterKnife.bind(this, view)

View File

@@ -24,6 +24,7 @@ import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import androidx.core.view.isGone
import androidx.core.view.isInvisible
@@ -107,6 +108,12 @@ class BottomSheetActionButton @JvmOverloads constructor(
leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
}
var titleTextColor: Int? = null
set(value) {
field = value
value?.let { actionTextView.setTextColor(it) }
}
init {
inflate(context, R.layout.item_verification_action, this)
ButterKnife.bind(this)
@@ -120,6 +127,7 @@ class BottomSheetActionButton @JvmOverloads constructor(
rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ContextCompat.getColor(context, R.color.riotx_accent))
}
}
}

View File

@@ -55,6 +55,10 @@ fun isAirplaneModeOn(context: Context): Boolean {
return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
}
fun isAnimationDisabled(context: Context): Boolean {
return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
}
/**
* display the system dialog for granting this permission. If previously granted, the
* system will not show it (so you should call this method).

View File

@@ -28,6 +28,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction {
object Cancel : SharedSecureStorageAction()
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction()
object ForgotResetAll : SharedSecureStorageAction()
object DoResetAll : SharedSecureStorageAction()
}
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
@@ -40,4 +42,5 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents {
object ShowModalLoading : SharedSecureStorageViewEvent()
object HideModalLoading : SharedSecureStorageViewEvent()
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
object ShowResetBottomSheet : SharedSecureStorageViewEvent()
}

View File

@@ -31,12 +31,14 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.crypto.recover.SetupMode
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import javax.inject.Inject
import kotlin.reflect.KClass
class SharedSecureStorageActivity : SimpleFragmentActivity() {
class SharedSecureStorageActivity : SimpleFragmentActivity(), VectorBaseBottomSheetDialogFragment.ResultListener {
@Parcelize
data class Args(
@@ -69,18 +71,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
private fun renderState(state: SharedSecureStorageViewState) {
if (!state.ready) return
val fragment = if (state.hasPassphrase) {
if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class
} else SharedSecuredStorageKeyFragment::class
val fragment =
when (state.step) {
SharedSecureStorageViewState.Step.EnterPassphrase -> SharedSecuredStoragePassphraseFragment::class
SharedSecureStorageViewState.Step.EnterKey -> SharedSecuredStorageKeyFragment::class
SharedSecureStorageViewState.Step.ResetAll -> SharedSecuredStorageResetAllFragment::class
}
showFragment(fragment, Bundle())
}
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
when (it) {
is SharedSecureStorageViewEvent.Dismiss -> {
is SharedSecureStorageViewEvent.Dismiss -> {
finish()
}
is SharedSecureStorageViewEvent.Error -> {
is SharedSecureStorageViewEvent.Error -> {
AlertDialog.Builder(this)
.setTitle(getString(R.string.dialog_title_error))
.setMessage(it.message)
@@ -92,21 +98,31 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
}
.show()
}
is SharedSecureStorageViewEvent.ShowModalLoading -> {
is SharedSecureStorageViewEvent.ShowModalLoading -> {
showWaitingView()
}
is SharedSecureStorageViewEvent.HideModalLoading -> {
is SharedSecureStorageViewEvent.HideModalLoading -> {
hideWaitingView()
}
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
updateWaitingView(it.waitingData)
}
is SharedSecureStorageViewEvent.FinishSuccess -> {
is SharedSecureStorageViewEvent.FinishSuccess -> {
val dataResult = Intent()
dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
setResult(Activity.RESULT_OK, dataResult)
finish()
}
is SharedSecureStorageViewEvent.ShowResetBottomSheet -> {
navigator.open4SSetup(this, SetupMode.HARD_RESET)
}
}
}
override fun onAttachFragment(fragment: Fragment) {
super.onAttachFragment(fragment)
if (fragment is VectorBaseBottomSheetDialogFragment) {
fragment.resultListener = this
}
}
@@ -124,6 +140,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
companion object {
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(context: Context,
@@ -140,4 +157,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
}
}
}
override fun onBottomSheetResult(resultCode: Int, data: Any?) {
if (resultCode == VectorBaseBottomSheetDialogFragment.ResultListener.RESULT_OK) {
// the 4S has been reset
setResult(Activity.RESULT_OK, Intent().apply { putExtra(EXTRA_DATA_RESET, true) })
finish()
}
}
}

View File

@@ -33,6 +33,9 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
@@ -40,19 +43,26 @@ import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.util.awaitCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import java.io.ByteArrayOutputStream
data class SharedSecureStorageViewState(
val ready: Boolean = false,
val hasPassphrase: Boolean = true,
val useKey: Boolean = false,
val passphraseVisible: Boolean = false,
val checkingSSSSAction: Async<Unit> = Uninitialized
) : MvRxState
val checkingSSSSAction: Async<Unit> = Uninitialized,
val step: Step = Step.EnterPassphrase,
val activeDeviceCount: Int = 0,
val showResetAllAction: Boolean = false,
val userId: String = ""
) : MvRxState {
enum class Step {
EnterPassphrase,
EnterKey,
ResetAll
}
}
class SharedSecureStorageViewModel @AssistedInject constructor(
@Assisted initialState: SharedSecureStorageViewState,
@@ -67,6 +77,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
}
init {
setState {
copy(userId = session.myUserId)
}
val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
if (!isValid) {
_viewEvents.post(
@@ -86,20 +100,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
if (info.content.passphrase != null) {
setState {
copy(
ready = true,
hasPassphrase = true,
useKey = false
ready = true,
step = SharedSecureStorageViewState.Step.EnterPassphrase
)
}
} else {
setState {
copy(
hasPassphrase = false,
ready = true,
hasPassphrase = false
step = SharedSecureStorageViewState.Step.EnterKey
)
}
}
}
session.rx()
.liveUserCryptoDevices(session.myUserId)
.distinctUntilChanged()
.execute {
copy(
activeDeviceCount = it.invoke()?.size ?: 0
)
}
}
override fun handle(action: SharedSecureStorageAction) = withState {
@@ -110,27 +134,52 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
SharedSecureStorageAction.UseKey -> handleUseKey()
is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action)
SharedSecureStorageAction.Back -> handleBack()
SharedSecureStorageAction.ForgotResetAll -> handleResetAll()
SharedSecureStorageAction.DoResetAll -> handleDoResetAll()
}.exhaustive
}
private fun handleDoResetAll() {
_viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet)
}
private fun handleResetAll() {
setState {
copy(
step = SharedSecureStorageViewState.Step.ResetAll
)
}
}
private fun handleUseKey() {
setState {
copy(
useKey = true
step = SharedSecureStorageViewState.Step.EnterKey
)
}
}
private fun handleBack() = withState { state ->
if (state.checkingSSSSAction is Loading) return@withState // ignore
if (state.hasPassphrase && state.useKey) {
setState {
copy(
useKey = false
)
when (state.step) {
SharedSecureStorageViewState.Step.EnterKey -> {
setState {
copy(
step = SharedSecureStorageViewState.Step.EnterPassphrase
)
}
}
SharedSecureStorageViewState.Step.ResetAll -> {
setState {
copy(
step = if (state.hasPassphrase) SharedSecureStorageViewState.Step.EnterPassphrase
else SharedSecureStorageViewState.Step.EnterKey
)
}
}
else -> {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
}
} else {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
}
}
@@ -158,6 +207,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also {
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) }
}
withContext(Dispatchers.IO) {

View File

@@ -27,9 +27,9 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startImportTextFromFileIntent
import org.matrix.android.sdk.api.extensions.tryOrNull
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
import org.matrix.android.sdk.api.extensions.tryOrNull
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -63,6 +63,10 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment
ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) }
ssss_key_reset.clickableView.debouncedClicks {
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
}
sharedViewModel.observeViewEvents {
when (it) {
is SharedSecureStorageViewEvent.KeyInlineError -> {

View File

@@ -74,6 +74,10 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
}
.disposeOnDestroyView()
ssss_passphrase_reset.clickableView.debouncedClicks {
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
}
sharedViewModel.observeViewEvents {
when (it) {
is SharedSecureStorageViewEvent.InlineError -> {

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2020 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.crypto.quads
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
import kotlinx.android.synthetic.main.fragment_ssss_reset_all.*
import javax.inject.Inject
class SharedSecuredStorageResetAllFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_ssss_reset_all
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ssss_reset_button_reset.debouncedClicks {
sharedViewModel.handle(SharedSecureStorageAction.DoResetAll)
}
ssss_reset_button_cancel.debouncedClicks {
sharedViewModel.handle(SharedSecureStorageAction.Back)
}
ssss_reset_other_devices.debouncedClicks {
withState(sharedViewModel) {
DeviceListBottomSheet.newInstance(it.userId, false).show(childFragmentManager, "DEV_LIST")
}
}
sharedViewModel.subscribe(this) { state ->
ssss_reset_other_devices.setTextOrHide(
state.activeDeviceCount
.takeIf { it > 0 }
?.let { resources.getQuantityString(R.plurals.secure_backup_reset_devices_you_can_verify, it, it) }
)
}
}
}

View File

@@ -45,8 +45,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
val initCrossSigningOnly: Boolean,
val forceReset4S: Boolean
val setUpMode: SetupMode = SetupMode.NORMAL
) : Parcelable
override val showExpanded = true
@@ -66,7 +65,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
super.onViewCreated(view, savedInstanceState)
viewModel.observeViewEvents { event ->
when (event) {
is BootstrapViewEvents.Dismiss -> dismiss()
is BootstrapViewEvents.Dismiss -> {
bottomSheetResult = if (event.success) ResultListener.RESULT_OK else ResultListener.RESULT_CANCEL
dismiss()
}
is BootstrapViewEvents.ModalError -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
@@ -90,6 +92,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
.setMessage(R.string.bootstrap_cancel_text)
.setPositiveButton(R.string._continue, null)
.setNegativeButton(R.string.skip) { _, _ ->
bottomSheetResult = ResultListener.RESULT_CANCEL
dismiss()
}
.show()
@@ -181,16 +184,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
const val EXTRA_ARGS = "EXTRA_ARGS"
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
BootstrapBottomSheet().apply {
fun show(fragmentManager: FragmentManager, mode: SetupMode): BootstrapBottomSheet {
return BootstrapBottomSheet().apply {
isCancelable = false
arguments = Bundle().apply {
this.putParcelable(EXTRA_ARGS, Args(
initCrossSigningOnly,
forceReset4S
))
this.putParcelable(EXTRA_ARGS, Args(setUpMode = mode))
}
}.show(fragmentManager, "BootstrapBottomSheet")
}.also {
it.show(fragmentManager, "BootstrapBottomSheet")
}
}
}

View File

@@ -69,10 +69,10 @@ interface BootstrapProgressListener {
data class Params(
val userPasswordAuth: UserPasswordAuth? = null,
val initOnlyCrossSigning: Boolean = false,
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?,
val keySpec: SsssKeySpec? = null
val keySpec: SsssKeySpec? = null,
val setupMode: SetupMode
)
// TODO Rename to CreateServerRecovery
@@ -84,9 +84,13 @@ class BootstrapCrossSigningTask @Inject constructor(
override suspend fun execute(params: Params): BootstrapResult {
val crossSigningService = session.cryptoService().crossSigningService()
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...")
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Starting...")
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) {
val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized()
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown())
|| (params.setupMode == SetupMode.HARD_RESET)
if (shouldSetCrossSigning) {
Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
params.progressListener?.onProgress(
WaitingViewData(
@@ -99,7 +103,7 @@ class BootstrapCrossSigningTask @Inject constructor(
awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
}
if (params.initOnlyCrossSigning) {
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
return BootstrapResult.SuccessCrossSigningOnly
}
} catch (failure: Throwable) {
@@ -107,7 +111,7 @@ class BootstrapCrossSigningTask @Inject constructor(
}
} else {
Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
if (params.initOnlyCrossSigning) {
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
// not sure how this can happen??
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
}
@@ -236,7 +240,13 @@ class BootstrapCrossSigningTask @Inject constructor(
val serverVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
if (serverVersion == null) {
val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version
val shouldCreateKeyBackup = serverVersion == null
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown)
|| (params.setupMode == SetupMode.HARD_RESET)
if (shouldCreateKeyBackup) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
@@ -260,16 +270,15 @@ class BootstrapCrossSigningTask @Inject constructor(
} else {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
// ensure we store existing backup secret if we have it!
val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
if (knownSecret != null && knownSecret.version == serverVersion.version) {
if (isMegolmBackupSecretKnown) {
// check it matches
val isValid = awaitCallback<Boolean> {
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey, it)
}
if (isValid) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
awaitCallback<Unit> {
extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
@@ -286,7 +295,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
}
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Finished")
return BootstrapResult.Success(keyInfo)
}

View File

@@ -34,6 +34,8 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
@@ -41,8 +43,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.OutputStream
class BootstrapSharedViewModel @AssistedInject constructor(
@@ -69,46 +69,52 @@ class BootstrapSharedViewModel @AssistedInject constructor(
init {
if (args.forceReset4S) {
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
}
} else if (args.initCrossSigningOnly) {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
}
} else {
// need to check if user have an existing keybackup
setState {
copy(step = BootstrapStep.CheckingMigration)
}
// We need to check if there is an existing backup
viewModelScope.launch(Dispatchers.IO) {
val version = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
when (args.setUpMode) {
SetupMode.PASSPHRASE_RESET,
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
SetupMode.HARD_RESET -> {
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
}
if (version == null) {
// we just resume plain bootstrap
doesKeyBackupExist = false
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
}
SetupMode.CROSS_SIGNING_ONLY -> {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
}
}
SetupMode.NORMAL -> {
// need to check if user have an existing keybackup
setState {
copy(step = BootstrapStep.CheckingMigration)
}
// We need to check if there is an existing backup
viewModelScope.launch(Dispatchers.IO) {
val version = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
val keyVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
}
if (keyVersion == null) {
// strange case... just finish?
_viewEvents.post(BootstrapViewEvents.Dismiss)
} else {
doesKeyBackupExist = true
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
if (version == null) {
// we just resume plain bootstrap
doesKeyBackupExist = false
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
val keyVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
}
if (keyVersion == null) {
// strange case... just finish?
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
} else {
doesKeyBackupExist = true
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
}
}
}
}
}
@@ -234,7 +240,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
BootstrapActions.Completed -> {
_viewEvents.post(BootstrapViewEvents.Dismiss)
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
BootstrapActions.GoToCompleted -> {
setState {
@@ -395,16 +401,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
bootstrapTask.invoke(this,
Params(
userPasswordAuth = userPasswordAuth,
initOnlyCrossSigning = args.initCrossSigningOnly,
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
setupMode = args.setUpMode
)
) { bootstrapResult ->
when (bootstrapResult) {
is BootstrapResult.SuccessCrossSigningOnly -> {
is BootstrapResult.SuccessCrossSigningOnly -> {
// TPD
_viewEvents.post(BootstrapViewEvents.Dismiss)
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
is BootstrapResult.Success -> {
setState {
@@ -428,7 +434,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
is BootstrapResult.UnsupportedAuthFlow -> {
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
_viewEvents.post(BootstrapViewEvents.Dismiss)
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
}
is BootstrapResult.InvalidPasswordError -> {
// it's a bad password
@@ -522,7 +528,13 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
BootstrapStep.CheckingMigration -> Unit
is BootstrapStep.FirstForm -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
_viewEvents.post(
when (args.setUpMode) {
SetupMode.CROSS_SIGNING_ONLY,
SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap()
else -> BootstrapViewEvents.Dismiss(success = false)
}
)
}
is BootstrapStep.GetBackupSecretForMigration -> {
setState {
@@ -558,7 +570,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
?: BootstrapBottomSheet.Args(SetupMode.CROSS_SIGNING_ONLY)
return fragment.bootstrapViewModelFactory.create(state, args)
}
}

View File

@@ -19,7 +19,7 @@ package im.vector.app.features.crypto.recover
import im.vector.app.core.platform.VectorViewEvents
sealed class BootstrapViewEvents : VectorViewEvents {
object Dismiss : BootstrapViewEvents()
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
data class ModalError(val error: String) : BootstrapViewEvents()
object RecoveryKeySaved: BootstrapViewEvents()
data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents()

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 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.crypto.recover
enum class SetupMode {
/**
* Only setup cross signing, no 4S or megolm backup
*/
CROSS_SIGNING_ONLY,
/**
* Normal setup mode.
*/
NORMAL,
/**
* Only reset the 4S passphrase/key, but do not touch
* to existing cross-signing or megolm backup
* It take the local known secrets and put them in 4S
*/
PASSPHRASE_RESET,
/**
* Resets the passphrase/key, and all missing secrets
* are re-created. Meaning that if cross signing is setup and the secrets
* keys are not known, cross signing will be reset (if secret is known we just keep same cross signing)
* Same apply to megolm
*/
PASSPHRASE_AND_NEEDED_SECRETS_RESET,
/**
* Resets the passphrase/key, cross signing and megolm backup
*/
HARD_RESET
}

View File

@@ -31,4 +31,5 @@ sealed class VerificationAction : VectorViewModelAction {
object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
object SecuredStorageHasBeenReset : VerificationAction()
}

View File

@@ -48,6 +48,7 @@ import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrS
import im.vector.app.features.crypto.verification.request.VerificationRequestFragment
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.settings.VectorSettingsActivity
import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@@ -55,7 +56,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import javax.inject.Inject
import kotlin.reflect.KClass
@@ -76,6 +76,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
@Inject
lateinit var avatarRenderer: AvatarRenderer
@@ -146,8 +147,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
val result = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
val reset = data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
if (result != null) {
viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
} else if (reset) {
// all have been reset, so we are verified?
viewModel.handle(VerificationAction.SecuredStorageHasBeenReset)
}
}
super.onActivityResult(requestCode, resultCode, data)
@@ -182,6 +188,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
if (state.quadSHasBeenReset) {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
isSuccessFull = true,
isMe = true,
cancelReason = null
))
})
return@withState
}
if (state.userThinkItsNotHim) {
otherUserNameText.text = getString(R.string.dialog_title_warning)
showFragment(VerificationNotMeFragment::class, Bundle())
@@ -356,6 +373,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
}
fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {

View File

@@ -31,6 +31,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@@ -74,7 +75,8 @@ data class VerificationBottomSheetViewState(
val currentDeviceCanCrossSign: Boolean = false,
val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false,
val quadSContainsSecrets: Boolean = true
val quadSContainsSecrets: Boolean = true,
val quadSHasBeenReset: Boolean = false
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(
@@ -349,6 +351,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
is VerificationAction.GotResultFromSsss -> {
handleSecretBackFromSSSS(action)
}
VerificationAction.SecuredStorageHasBeenReset -> {
if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) {
setState {
copy(quadSHasBeenReset = true)
}
}
Unit
}
}.exhaustive
}
@@ -393,7 +403,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
private fun tentativeRestoreBackup(res: Map<String, String>?) {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
try {
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
Timber.v("## Keybackup secret not restored from SSSS")

View File

@@ -53,6 +53,8 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader
import com.airbnb.epoxy.glidePreloader
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
@@ -75,6 +77,7 @@ import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.extensions.trackItemsVisibilityChange
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@@ -218,7 +221,8 @@ class RoomDetailFragment @Inject constructor(
private val colorProvider: ColorProvider,
private val notificationUtils: NotificationUtils,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider
private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer
) :
VectorBaseFragment(),
TimelineEventController.Callback,
@@ -700,7 +704,13 @@ class RoomDetailFragment @Inject constructor(
// safeStartCall(it, isVideoCall)
// }
} else if (!state.isAllowedToStartWebRTCCall) {
showDialogWithMessage(getString(R.string.no_permissions_to_start_webrtc_call))
showDialogWithMessage(getString(
if (state.isDm()) {
R.string.no_permissions_to_start_webrtc_call_in_direct_room
} else {
R.string.no_permissions_to_start_webrtc_call
})
)
} else {
safeStartCall(isVideoCall)
}
@@ -710,7 +720,13 @@ class RoomDetailFragment @Inject constructor(
// can you add widgets??
if (!state.isAllowedToManageWidgets) {
// You do not have permission to start a conference call in this room
showDialogWithMessage(getString(R.string.no_permissions_to_start_conf_call))
showDialogWithMessage(getString(
if (state.isDm()) {
R.string.no_permissions_to_start_conf_call_in_direct_room
} else {
R.string.no_permissions_to_start_conf_call
}
))
} else {
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
// A conference is already in progress!
@@ -921,6 +937,16 @@ class RoomDetailFragment @Inject constructor(
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
}
recyclerView.addGlidePreloader(
epoxyController = timelineEventController,
requestManager = GlideApp.with(this),
preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ ->
imageContentRenderer.createGlideRequest(
epoxyModel.mediaData,
ImageContentRenderer.Mode.THUMBNAIL,
requestManager as GlideRequests
)
})
}
private fun updateJumpToReadMarkerViewVisibility() {

View File

@@ -77,4 +77,6 @@ data class RoomDetailViewState(
// Also highlight the target event, if any
highlightedEventId = args.eventId
)
fun isDm() = asyncRoomSummary()?.isDirect == true
}

View File

@@ -51,18 +51,28 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val session: Session,
@TimelineEventControllerHandler
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
@@ -114,9 +124,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false
private var hasReachedInvite: Boolean = false
private var hasUTD: Boolean = false
private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null
private var previousModelsSize = 0
var callback: Callback? = null
var timeline: Timeline? = null
@@ -193,6 +206,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
models.add(position, readMarker)
}
}
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
val indexOfPrefetchBackward = (previousModelsSize - 1)
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
.coerceAtLeast(0)
val loadingItem = LoadingItem_()
.id("prefetch_backward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
models.add(indexOfPrefetchBackward, loadingItem)
}
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
if (shouldAddForwardPrefetch) {
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
val loadingItem = LoadingItem_()
.id("prefetch_forward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
models.add(indexOfPrefetchForward, loadingItem)
}
previousModelsSize = models.size
}
fun update(viewState: RoomDetailViewState) {
@@ -243,7 +279,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val timelineModels = getModels()
add(timelineModels)
if (hasReachedInvite && hasUTD) {
return
}
// Avoid displaying two loaders if there is no elements between them
val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty()
// We can hide the loader but still add the item to controller so it can trigger backwards pagination
@@ -303,6 +341,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
hasUTD = false
hasReachedInvite = false
if (modelCache.isEmpty()) {
return
}
@@ -318,13 +359,21 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition)
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null)
}
updateUTDStates(event, nextEvent)
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val addDaySeparator = if (hasReachedInvite && hasUTD) {
true
} else {
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
date.toLocalDate() != nextDate?.toLocalDate()
}
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent,
items = items,
@@ -348,6 +397,27 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
return
}
if (event.root.type == EventType.STATE_ROOM_MEMBER
&& event.root.stateKey == session.myUserId) {
val content = event.root.content.toModel<RoomMemberContent>()
if (content?.membership == Membership.INVITE) {
hasReachedInvite = true
} else if (content?.membership == Membership.JOIN) {
val prevContent = event.root.resolvedPrevContent().toModel<RoomMemberContent>()
if (prevContent?.membership?.isActive() == false) {
hasReachedInvite = true
}
}
}
if (nextEvent?.root?.getClearType() == EventType.ENCRYPTED) {
hasUTD = true
}
}
/**
* Return true if added
*/
@@ -357,9 +427,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return shouldAdd
}
/**
* Return true if added
*/
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {

View File

@@ -190,7 +190,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
noticeEventFormatter.format(timelineEvent)
noticeEventFormatter.format(timelineEvent, room?.roomSummary())
}
else -> null
} ?: ""

View File

@@ -25,6 +25,8 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@@ -36,7 +38,8 @@ class EncryptionItemFactory @Inject constructor(
private val messageColorProvider: MessageColorProvider,
private val stringProvider: StringProvider,
private val informationDataFactory: MessageInformationDataFactory,
private val avatarSizeProvider: AvatarSizeProvider) {
private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent,
highlight: Boolean,
@@ -51,7 +54,13 @@ class EncryptionItemFactory @Inject constructor(
val shield: StatusTileTimelineItem.ShieldUIState
if (isSafeAlgorithm) {
title = stringProvider.getString(R.string.encryption_enabled)
description = stringProvider.getString(R.string.encryption_enabled_tile_description)
description = stringProvider.getString(
if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) {
R.string.direct_room_encryption_enabled_tile_description
} else {
R.string.encryption_enabled_tile_description
}
)
shield = StatusTileTimelineItem.ShieldUIState.BLACK
} else {
title = stringProvider.getString(R.string.encryption_not_enabled)

View File

@@ -22,6 +22,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
@@ -30,22 +31,19 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedUTDItem
import im.vector.app.features.home.room.detail.timeline.item.MergedUTDItem_
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import timber.log.Timber
import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider,
private val vectorPreferences: VectorPreferences) {
private val roomSummaryHolder: RoomSummaryHolder) {
private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@@ -63,10 +61,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit)
: BasedMergedItem<*>? {
return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) {
Timber.v("## MERGE: Candidate for merge, top event ${event.eventId}")
buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, /*requestModelBuild,*/ callback)
} else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
// It's the first item before room.create
// Collapse all room configuration events
@@ -78,6 +73,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
}
}
private fun isDirectRoom() = roomSummaryHolder.roomSummary?.isDirect.orFalse()
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
items: List<TimelineEvent>,
event: TimelineEvent,
@@ -100,7 +97,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
avatarUrl = mergedEvent.senderInfo.avatarUrl,
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
eventId = mergedEvent.root.eventId ?: "",
isDirectRoom = isDirectRoom()
)
mergedData.add(data)
}
@@ -138,82 +136,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
}
}
// Event should be UTD
// Next event should not
private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
if (!vectorPreferences.mergeUTDinTimeline()) return false
// if event is not UTD return false
if (!isEventUTD(event)) return false
// At this point event cannot be decrypted
// Let's check if older event is not UTD
return nextEvent == null || !isEventUTD(event)
}
private fun isEventUTD(event: TimelineEvent): Boolean {
return event.root.getClearType() == EventType.ENCRYPTED && !event.root.isRedacted()
}
private fun buildUTDMergedSummary(currentPosition: Int,
items: List<TimelineEvent>,
event: TimelineEvent,
eventIdToHighlight: String?,
// requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedUTDItem_? {
Timber.v("## MERGE: buildUTDMergedSummary from position $currentPosition")
var prevEvent = items.prevOrNull(currentPosition)
var tmpPos = currentPosition - 1
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
while (prevEvent != null && isEventUTD(prevEvent)) {
mergedEvents.add(prevEvent)
tmpPos--
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
}
Timber.v("## MERGE: buildUTDMergedSummary merge group size ${mergedEvents.size}")
if (mergedEvents.size < 3) return null
var highlighted = false
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.reversed()
.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
val senderAvatar = mergedEvent.senderInfo.avatarUrl
val senderName = mergedEvent.senderInfo.disambiguatedDisplayName
val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
)
mergedData.add(data)
}
val mergedEventIds = mergedEvents.map { it.localId }
collapsedEventIds.addAll(mergedEventIds)
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedUTDItem.Attributes(
isCollapsed = true,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {}
)
return MergedUTDItem_()
.id(mergeId)
.big(mergedEventIds.size > 5)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlighted)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List<TimelineEvent>,
event: TimelineEvent,
@@ -247,7 +169,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
avatarUrl = mergedEvent.senderInfo.avatarUrl,
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
eventId = mergedEvent.root.eventId ?: "",
isDirectRoom = isDirectRoom()
)
mergedData.add(data)
}

View File

@@ -38,6 +38,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
@@ -101,6 +102,7 @@ class MessageItemFactory @Inject constructor(
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
private val roomSummaryHolder: RoomSummaryHolder,
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
@@ -130,7 +132,7 @@ class MessageItemFactory @Inject constructor(
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) {
// This is an edit event, we should display it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback)
return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
}
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
@@ -146,7 +148,7 @@ class MessageItemFactory @Inject constructor(
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}

View File

@@ -24,6 +24,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
@@ -34,8 +35,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
fun create(event: TimelineEvent,
highlight: Boolean,
roomSummary: RoomSummary?,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val formattedText = eventFormatter.format(event, roomSummary) ?: return null
val informationData = informationDataFactory.create(event, null)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,

View File

@@ -21,6 +21,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.Session
@@ -32,6 +33,7 @@ import javax.inject.Inject
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session,
private val roomSummaryHolder: RoomSummaryHolder,
private val noticeItemFactory: NoticeItemFactory) {
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
@@ -52,7 +54,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, false, callback)
noticeItemFactory.create(event, false, roomSummaryHolder.roomSummary, callback)
} else {
null
}

View File

@@ -20,6 +20,7 @@ import im.vector.app.core.epoxy.EmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
@@ -31,6 +32,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val defaultItemFactory: DefaultItemFactory,
private val encryptionItemFactory: EncryptionItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory,
private val roomSummaryHolder: RoomSummaryHolder,
private val verificationConclusionItemFactory: VerificationItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) {
@@ -63,10 +65,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_ANSWER,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.REACTION,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> {
encryptionItemFactory.create(event, highlight, callback)
}
EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto
@@ -87,7 +87,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
// TODO These are not filtered out by timeline when encrypted
// For now manually ignore
if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, highlight, callback)
noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
} else {
null
}

View File

@@ -24,6 +24,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
import org.matrix.android.sdk.api.session.Session
@@ -50,6 +51,7 @@ class VerificationItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory,
private val userPreferencesProvider: UserPreferencesProvider,
private val roomSummaryHolder: RoomSummaryHolder,
private val stringProvider: StringProvider,
private val session: Session
) {
@@ -151,7 +153,7 @@ class VerificationItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
return null
}
}

View File

@@ -23,6 +23,7 @@ import im.vector.app.core.resources.StringProvider
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
@@ -40,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor(
private val noticeEventFormatter: NoticeEventFormatter
) {
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean, roomSummary: RoomSummary?): CharSequence {
if (timelineEvent.root.isRedacted()) {
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
}
@@ -130,7 +131,7 @@ class DisplayableEventFormatter @Inject constructor(
}
else -> {
return span {
text = noticeEventFormatter.format(timelineEvent) ?: ""
text = noticeEventFormatter.format(timelineEvent, roomSummary) ?: ""
textStyle = "italic"
}
}

View File

@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
@@ -56,23 +57,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun format(timelineEvent: TimelineEvent): CharSequence? {
private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
fun format(timelineEvent: TimelineEvent, rs: RoomSummary?): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root)
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_WIDGET,
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.CALL_INVITE,
EventType.CALL_CANDIDATES,
@@ -151,19 +155,19 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
fun format(event: Event, senderName: String?): CharSequence? {
fun format(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
return when (val type = event.getClearType()) {
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName)
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, rs)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs)
else -> {
Timber.v("Type $type not handled by this formatter")
null
@@ -175,14 +179,14 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
return "{ \"type\": ${event.getClearType()} }"
}
private fun formatRoomCreateEvent(event: Event): CharSequence? {
private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? {
return event.getClearContent().toModel<RoomCreateContent>()
?.takeIf { it.creator.isNullOrBlank().not() }
?.let {
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_created_by_you)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
} else {
sp.getString(R.string.notice_room_created, it.creator)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
}
}
}
@@ -204,11 +208,11 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? {
private fun formatRoomTombstoneEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
return if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_update_by_you)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
} else {
sp.getString(R.string.notice_room_update, senderName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
}
}
@@ -246,18 +250,20 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility)
return if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_made_future_room_visibility_by_you, formattedVisibility)
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
formattedVisibility)
} else {
sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
senderName, formattedVisibility)
}
}
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?): CharSequence? {
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
@@ -265,17 +271,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
prevContent != null -> {
// Revoke case
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_third_party_revoked_invite_by_you, prevContent.displayName)
sp.getString(
if (rs.isDm()) {
R.string.notice_direct_room_third_party_revoked_invite_by_you
} else {
R.string.notice_room_third_party_revoked_invite_by_you
},
prevContent.displayName)
} else {
sp.getString(R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
senderName, prevContent.displayName)
}
}
content != null -> {
// Invitation case
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_third_party_invite_by_you, content.displayName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
content.displayName)
} else {
sp.getString(R.string.notice_room_third_party_invite, senderName, content.displayName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
senderName, content.displayName)
}
}
else -> null
@@ -323,13 +338,13 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
private fun formatRoomMemberEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|| eventContent?.membership == Membership.LEAVE
return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs)
} else {
buildProfileNotice(event, senderName, eventContent, prevEventContent)
}
@@ -387,20 +402,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?): String? {
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
return when (eventContent?.guestAccess) {
GuestAccess.CanJoin ->
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_guest_access_can_join_by_you)
sp.getString(
if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
)
} else {
sp.getString(R.string.notice_room_guest_access_can_join, senderName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
senderName)
}
GuestAccess.Forbidden ->
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_guest_access_forbidden_by_you)
sp.getString(
if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
)
} else {
sp.getString(R.string.notice_room_guest_access_forbidden, senderName)
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
senderName)
}
else -> null
}
@@ -476,7 +497,11 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
return displayText.toString()
}
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? {
private fun buildMembershipNotice(event: Event,
senderName: String?,
eventContent: RoomMemberContent?,
prevEventContent: RoomMemberContent?,
rs: RoomSummary?): String? {
val senderDisplayName = senderName ?: event.senderId ?: ""
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) {
@@ -524,14 +549,21 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
Membership.JOIN ->
if (event.isSentByCurrentUser()) {
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_join_with_reason_by_you, reason)
} ?: sp.getString(R.string.notice_room_join_by_you)
} else {
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason)
} ?: sp.getString(R.string.notice_room_join, senderDisplayName)
eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
reason)
} else {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
senderDisplayName, reason)
}
} ?: run {
if (event.isSentByCurrentUser()) {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
} else {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join else R.string.notice_room_join,
senderDisplayName)
}
}
Membership.LEAVE ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
@@ -548,14 +580,27 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
} ?: sp.getString(R.string.notice_room_reject, senderDisplayName)
}
else ->
if (event.isSentByCurrentUser()) {
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_leave_with_reason_by_you, reason)
} ?: sp.getString(R.string.notice_room_leave_by_you)
} else {
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason)
} ?: sp.getString(R.string.notice_room_leave, senderDisplayName)
eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) {
sp.getString(
if (rs.isDm()) {
R.string.notice_direct_room_leave_with_reason_by_you
} else {
R.string.notice_room_leave_with_reason_by_you
},
reason
)
} else {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
senderDisplayName, reason)
}
} ?: run {
if (event.isSentByCurrentUser()) {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
} else {
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave else R.string.notice_room_leave,
senderDisplayName)
}
}
}
} else {
@@ -618,14 +663,15 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
}
}
private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? {
private fun formatJoinRulesEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
return when (content.joinRules) {
RoomJoinRules.INVITE ->
if (event.isSentByCurrentUser()) {
sp.getString(R.string.room_join_rules_invite_by_you)
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
} else {
sp.getString(R.string.room_join_rules_invite, senderName)
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
senderName)
}
RoomJoinRules.PUBLIC ->
if (event.isSentByCurrentUser()) {

View File

@@ -62,7 +62,8 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
val eventId: String,
val userId: String,
val memberName: String,
val avatarUrl: String?
val avatarUrl: String?,
val isDirectRoom: Boolean
)
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)

View File

@@ -48,9 +48,17 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
val createdFromCurrentUser = data?.userId == attributes.currentUserId
val summary = if (createdFromCurrentUser) {
holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_created_summary_item_by_you)
} else {
holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
}
} else {
holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_created_summary_item, data.memberName)
} else {
holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
}
}
holder.summaryView.text = summary
holder.summaryView.visibility = View.VISIBLE
@@ -69,7 +77,11 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
} else {
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
}
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),

View File

@@ -1,127 +0,0 @@
/*
* Copyright (c) 2020 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.detail.timeline.item
import android.util.TypedValue
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedUTDItem : BasedMergedItem<MergedUTDItem.Holder>() {
@EpoxyAttribute
override lateinit var attributes: Attributes
@EpoxyAttribute
var big: Boolean? = false
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
holder.mergedTile.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
if (big == true) {
this.height = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
800f,
holder.view.context.resources.displayMetrics
).toInt()
} else {
this.height = LinearLayout.LayoutParams.WRAP_CONTENT
}
}
// if (attributes.isCollapsed) {
// // Take the oldest data
// val data = distinctMergeData.lastOrNull()
//
// val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
// data?.memberName ?: data?.userId ?: "")
// holder.summaryView.text = summary
// holder.summaryView.visibility = View.VISIBLE
// holder.avatarView.visibility = View.VISIBLE
// if (data != null) {
// holder.avatarView.visibility = View.VISIBLE
// attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
// } else {
// holder.avatarView.visibility = View.GONE
// }
//
// if (attributes.hasEncryptionEvent) {
// holder.encryptionTile.isVisible = true
// holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
// this.marginEnd = leftGuideline
// }
// if (attributes.isEncryptionAlgorithmSecure) {
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
// holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
// null, null, null
// )
// } else {
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
// null, null, null
// )
// }
// } else {
// holder.encryptionTile.isVisible = false
// }
// } else {
// holder.avatarView.visibility = View.INVISIBLE
// holder.summaryView.visibility = View.GONE
// holder.encryptionTile.isGone = true
// }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
// val summaryView by bind<TextView>(R.id.itemNoticeTextView)
// val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
val mergedTile by bind<ViewGroup>(R.id.mergedUTDTile)
//
// val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
// val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
}
companion object {
private const val STUB_ID = R.id.messageContentMergedUTDStub
}
data class Attributes(
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes
}

View File

@@ -86,7 +86,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestPreviewableEvent
if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not(), roomSummary)
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
}
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)

View File

@@ -146,8 +146,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
LoginServerSelectionFragment::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})

View File

@@ -33,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.ui.model.Size
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.isLocalFile
@@ -206,12 +207,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
}
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
return if (data.elementToDecrypt != null) {
// Encrypted image
GlideApp
.with(imageView)
.load(data)
glideRequests.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
@@ -223,15 +226,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
// Fallback to base url
?: data.url.takeIf { it?.startsWith("content://") == true }
GlideApp
.with(imageView)
glideRequests
.load(resolvedUrl)
.apply {
if (mode == Mode.THUMBNAIL) {
error(
GlideApp
.with(imageView)
.load(resolveUrl(data))
glideRequests.load(resolveUrl(data))
)
}
}

View File

@@ -37,6 +37,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity
@@ -153,7 +154,10 @@ class DefaultNavigator @Inject constructor(
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
if (context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
BootstrapBottomSheet.show(
context.supportFragmentManager,
if (initCrossSigningOnly) SetupMode.CROSS_SIGNING_ONLY else SetupMode.NORMAL
)
}
}
@@ -226,13 +230,19 @@ class DefaultNavigator @Inject constructor(
// if cross signing is enabled we should propose full 4S
sessionHolder.getSafeActiveSession()?.let { session ->
if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
} else {
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
}
}
}
override fun open4SSetup(context: Context, setupMode: SetupMode) {
if (context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, setupMode)
}
}
override fun openKeysBackupManager(context: Context) {
context.startActivity(KeysBackupManageActivity.intent(context))
}

View File

@@ -21,6 +21,7 @@ import android.content.Context
import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinActivity
@@ -71,6 +72,8 @@ interface Navigator {
fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
fun open4SSetup(context: Context, setupMode: SetupMode)
fun openKeysBackupManager(context: Context)
fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false)

View File

@@ -91,7 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
if (room == null) {
Timber.e("## Unable to resolve room for eventId [$event]")
// Ok room is not known in store, but we can still display something
val body = displayableEventFormatter.format(event, false)
val body = displayableEventFormatter.format(event, false, null)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
@@ -124,7 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
}
}
val body = displayableEventFormatter.format(event, false).toString()
val body = displayableEventFormatter.format(event, false, room.roomSummary()).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
@@ -165,7 +165,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val roomId = event.roomId ?: return null
val dName = event.senderId?.let { session.getUser(it)?.displayName }
if (Membership.INVITE == content.membership) {
val body = noticeEventFormatter.format(event, dName)
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
?: stringProvider.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent(
session.myUserId,

View File

@@ -81,6 +81,11 @@ class PinLocker @Inject constructor(
computeState()
}
fun screenIsOff() {
shouldBeLocked = true
computeState()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs

View File

@@ -27,6 +27,7 @@ import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.isAnimationDisabled
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.pin.PinActivity
import im.vector.app.features.themes.ThemeUtils
@@ -172,6 +173,8 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
clearLightStatusBar()
val noAnimation = !animate || isAnimationDisabled(activity)
alert.weakCurrentActivity = WeakReference(activity)
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
else Alerter.create(activity)
@@ -187,7 +190,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
}
}
.apply {
if (!animate) {
if (noAnimation) {
setEnterAnimation(R.anim.anim_alerter_no_anim)
}
@@ -237,6 +240,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
}
}
.enableIconPulse(!noAnimation)
.show()
}

View File

@@ -17,6 +17,7 @@
package im.vector.app.features.raw.wellknown
import com.squareup.moshi.JsonAdapter
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.MoshiProvider
object ElementWellKnownMapper {
@@ -24,6 +25,6 @@ object ElementWellKnownMapper {
val adapter: JsonAdapter<ElementWellKnown> = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java)
fun from(value: String): ElementWellKnown? {
return adapter.fromJson(value)
return tryOrNull("Unable to parse well-known data") { adapter.fromJson(value) }
}
}

View File

@@ -18,6 +18,7 @@ package im.vector.app.features.roommemberprofile.devices
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
@@ -29,6 +30,7 @@ import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import kotlinx.android.parcel.Parcelize
import javax.inject.Inject
import kotlin.reflect.KClass
@@ -104,10 +106,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
@Parcelize
data class Args(
val userId: String,
val allowDeviceAction: Boolean
) : Parcelable
companion object {
fun newInstance(userId: String): DeviceListBottomSheet {
fun newInstance(userId: String, allowDeviceAction: Boolean = true): DeviceListBottomSheet {
val args = Bundle()
args.putString(MvRx.KEY_ARG, userId)
args.putParcelable(MvRx.KEY_ARG, Args(userId, allowDeviceAction))
return DeviceListBottomSheet().apply { arguments = args }
}
}

View File

@@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.args
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.di.HasScreenInjector
@@ -44,24 +45,24 @@ data class DeviceListViewState(
) : MvRxState
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@Assisted private val userId: String,
@Assisted private val args: DeviceListBottomSheet.Args,
private val session: Session)
: VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel
}
init {
session.rx().liveUserCryptoDevices(userId)
session.rx().liveUserCryptoDevices(args.userId)
.execute {
copy(cryptoDevices = it).also {
refreshSelectedId()
}
}
session.rx().liveCrossSigningInfo(userId)
session.rx().liveCrossSigningInfo(args.userId)
.execute {
copy(memberCrossSigningKey = it.invoke()?.getOrNull())
}
@@ -88,6 +89,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
private fun selectDevice(action: DeviceListAction.SelectDevice) {
if (!args.allowDeviceAction) return
setState {
copy(selectedDevice = action.device)
}
@@ -100,8 +102,9 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
if (!args.allowDeviceAction) return
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, args.userId, action.deviceId, null)?.let { txID ->
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(args.userId, txID))
}
}
@@ -109,12 +112,12 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? {
val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val userId = viewModelContext.args<String>()
return fragment.viewModelFactory.create(state, userId)
val args = viewModelContext.args<DeviceListBottomSheet.Args>()
return fragment.viewModelFactory.create(state, args)
}
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
val userId = viewModelContext.args<String>()
val userId = viewModelContext.args<DeviceListBottomSheet.Args>().userId
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(userId)?.toMatrixItem()?.let {
DeviceListViewState(

View File

@@ -18,7 +18,6 @@
package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import im.vector.app.R
import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection
@@ -26,6 +25,7 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject
class RoomProfileController @Inject constructor(
@@ -57,9 +57,9 @@ class RoomProfileController @Inject constructor(
// Security
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
val learnMoreSubtitle = if (roomSummary.isEncrypted) {
R.string.room_profile_encrypted_subtitle
if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle
} else {
R.string.room_profile_not_encrypted_subtitle
if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle else R.string.room_profile_not_encrypted_subtitle
}
genericFooterItem {
id("e2e info")
@@ -71,7 +71,11 @@ class RoomProfileController @Inject constructor(
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "settings",
title = stringProvider.getString(R.string.room_profile_section_more_settings),
title = stringProvider.getString(if (roomSummary.isDirect) {
R.string.direct_room_profile_section_more_settings
} else {
R.string.room_profile_section_more_settings
}),
dividerColor = dividerColor,
icon = R.drawable.ic_room_profile_settings,
action = { callback?.onSettingsClicked() }
@@ -112,7 +116,11 @@ class RoomProfileController @Inject constructor(
)
buildProfileAction(
id = "leave",
title = stringProvider.getString(R.string.room_profile_section_more_leave),
title = stringProvider.getString(if (roomSummary.isDirect) {
R.string.direct_room_profile_section_more_leave
} else {
R.string.room_profile_section_more_leave
}),
dividerColor = dividerColor,
divider = false,
destructive = true,

View File

@@ -153,7 +153,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
// SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS
private const val SETTINGS_LABS_MERGE_E2E_ERRORS = "SETTINGS_LABS_MERGE_E2E_ERRORS"
private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
// analytics
@@ -285,8 +285,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
}
fun mergeUTDinTimeline(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_MERGE_E2E_ERRORS, false)
fun labShowCompleteHistoryInEncryptedRoom(): Boolean {
return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM, false)
}
fun labAllowedExtendedLogging(): Boolean {

View File

@@ -53,11 +53,12 @@ import im.vector.app.features.crypto.keys.KeysExporter
import im.vector.app.features.crypto.keys.KeysImporter
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.navigation.Navigator
import im.vector.app.features.pin.PinActivity
import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.PinMode
import im.vector.app.features.raw.wellknown.ElementWellKnownMapper
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -153,14 +154,13 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
disposables.add(it)
}
vectorActivity.getVectorComponent()
.rawService()
.getWellknown(session.myUserId, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
ElementWellKnownMapper.from(data)?.isE2EByDefault() == false
}
})
lifecycleScope.launchWhenResumed {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
vectorActivity.getVectorComponent()
.rawService()
.getElementWellknown(session.myUserId)
?.isE2EByDefault() == false
}
}
private val secureBackupCategory by lazy {
@@ -194,7 +194,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
true
}
} else {
@@ -213,7 +213,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.PASSPHRASE_RESET)
true
}
} else if (!info.megolmSecretKnown) {

View File

@@ -44,6 +44,7 @@ import im.vector.app.core.extensions.queryExportKeys
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
import im.vector.app.features.crypto.recover.SetupMode
import timber.log.Timber
import javax.inject.Inject
@@ -121,7 +122,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
super.onActivityCreated(savedInstanceState)
setupRecoveryButton.action = {
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
}
exitAnywayButton.action = {

View File

@@ -3,144 +3,187 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
android:background="?riotx_background"
android:paddingStart="36dp"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="36dp"
android:paddingBottom="@dimen/layout_vertical_margin">
<androidx.core.widget.NestedScrollView
<!-- Strategy: 5 Spaces are used to spread the remaining space, using weight -->
<Space
android:id="@+id/loginSplashSpace1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashLogoContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintVertical_weight="4" />
<LinearLayout
android:id="@+id/loginSplashLogoContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace1">
<androidx.constraintlayout.widget.ConstraintLayout
style="@style/LoginContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/loginSplashLogo"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/element_logo_green"
android:transitionName="loginLogoTransition" />
<ImageView
android:id="@+id/loginSplashLogo"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/element_logo_green"
android:transitionName="loginLogoTransition"
app:layout_constraintBottom_toTopOf="@+id/logoType"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/logoType"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginTop="8dp"
android:src="@drawable/element_logotype"
android:tint="?colorAccent" />
<ImageView
android:id="@+id/logoType"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginTop="8dp"
android:src="@drawable/element_logotype"
android:tint="?colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginSplashLogo" />
</LinearLayout>
<TextView
android:id="@+id/loginSplashTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="@string/login_splash_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
android:transitionName="loginTitleTransition"
app:layout_constraintBottom_toTopOf="@+id/loginSplashText1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logoType" />
<Space
android:id="@+id/loginSplashSpace2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle"
app:layout_constraintTop_toBottomOf="@+id/loginSplashLogoContainer"
app:layout_constraintVertical_weight="1" />
<ImageView
android:id="@+id/loginSplashPicto1"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_message_circle"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
<TextView
android:id="@+id/loginSplashTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/login_splash_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
android:transitionName="loginTitleTransition"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace2" />
<TextView
android:id="@+id/loginSplashText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="96dp"
android:gravity="start"
android:text="@string/login_splash_text1"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle" />
<Space
android:id="@+id/loginSplashSpace3"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashContent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle"
app:layout_constraintVertical_weight="2" />
<ImageView
android:id="@+id/loginSplashPicto2"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_lock"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginSplashContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace3">
<TextView
android:id="@+id/loginSplashText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
<ImageView
android:id="@+id/loginSplashPicto1"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_message_circle"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
<ImageView
android:id="@+id/loginSplashPicto3"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_sliders"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
<TextView
android:id="@+id/loginSplashText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="start"
android:text="@string/login_splash_text1"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/loginSplashText3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text3"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
<ImageView
android:id="@+id/loginSplashPicto2"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_lock"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSplashSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="@string/login_splash_submit"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText3" />
<TextView
android:id="@+id/loginSplashText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/loginSplashPicto3"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_login_splash_sliders"
android:tint="?riotx_text_secondary"
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/loginSplashText3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/login_splash_text3"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Space
android:id="@+id/loginSplashSpace4"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
app:layout_constraintTop_toBottomOf="@+id/loginSplashContent"
app:layout_constraintVertical_weight="2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginSplashSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_splash_submit"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace4" />
<Space
android:id="@+id/loginSplashSpace5"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSplashSubmit"
app:layout_constraintVertical_weight="4" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -15,11 +15,11 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@drawable/ic_security_key_24dp"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
android:src="@drawable/ic_security_key_24dp" />
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key" />
<TextView
android:id="@+id/ssss_restore_with_key"
@@ -56,8 +56,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -97,16 +97,32 @@
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_key_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toTopOf="@+id/ssss_key_reset"
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til"
app:layout_goneMarginBottom="@dimen/layout_vertical_margin_big" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/ssss_key_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
app:layout_constraintTop_toBottomOf="@id/ssss_key_flow"
app:leftIcon="@drawable/ic_alert_triangle"
app:tint="@color/vector_error_color"
app:titleTextColor="?riotx_text_secondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -109,16 +109,33 @@
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_passphrase_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toTopOf="@+id/ssss_passphrase_reset"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til"
app:layout_goneMarginBottom="32dp" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/ssss_passphrase_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
android:clickable="true"
android:focusable="true"
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_flow"
app:leftIcon="@drawable/ic_alert_triangle"
app:tint="@color/vector_error_color"
app:titleTextColor="?riotx_text_secondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ssss__root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/reset_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_alert_triangle"
android:drawablePadding="8dp"
android:drawableTint="?riot_primary_text_color"
android:text="@string/secure_backup_reset_all"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
android:tint="?riot_primary_text_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssss_reset_all_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_all_no_other_devices"
android:textColor="?riotx_text_primary"
app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices"
app:layout_constraintTop_toBottomOf="@id/reset_title" />
<TextView
android:id="@+id/ssss_reset_other_devices"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:drawableStart="@drawable/ic_smartphone"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:textSize="17sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_all_description"
tools:text="Show 2 devices you can verify with now"
tools:visibility="visible" />
<TextView
android:id="@+id/ssss_reset_text3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/secure_backup_reset_if_you_reset_all"
android:textColor="@color/riotx_destructive_accent"
android:textSize="15sp"
android:textStyle="bold"
android:tint="?riot_primary_text_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
<TextView
android:id="@+id/ssss_reset_text4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_no_history"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_reset_button_cancel"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_reset_button_reset"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_passphrase_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="16dp"
app:constraint_referenced_ids="ssss_reset_button_cancel, ssss_reset_button_reset"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text4" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -52,6 +52,7 @@
<declare-styleable name="BottomSheetActionButton">
<attr name="tint" format="color" />
<attr name="titleTextColor" format="color" />
<attr name="actionTitle" format="string" />
<attr name="actionDescription" format="string" />
<attr name="leftIcon" format="reference" />

View File

@@ -78,6 +78,7 @@
<string name="play_video">Play</string>
<string name="pause_video">Pause</string>
<string name="dismiss">Dismiss</string>
<string name="reset">Reset</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
@@ -89,7 +90,9 @@
<string name="missing_permissions_error">"Due to missing permissions, this action is not possible.</string>
<string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>
<string name="no_permissions_to_start_conf_call">You do not have permission to start a conference call in this room</string>
<string name="no_permissions_to_start_conf_call_in_direct_room">You do not have permission to start a conference call</string>
<string name="no_permissions_to_start_webrtc_call">You do not have permission to start a call in this room</string>
<string name="no_permissions_to_start_webrtc_call_in_direct_room">You do not have permission to start a call</string>
<string name="conference_call_in_progress">A conference is already in progress!</string>
<string name="video_meeting">Start video meeting</string>
<string name="audio_meeting">Start audio meeting</string>
@@ -1708,6 +1711,7 @@
<string name="send_suggestion_failed">The suggestion failed to be sent (%s)</string>
<string name="settings_labs_show_hidden_events_in_timeline">Show hidden events in timeline</string>
<string name="settings_labs_show_complete_history_in_encrypted_room">"Show complete history in encrypted rooms"</string>
<string name="bottom_action_people_x">Direct Messages</string>
@@ -1881,6 +1885,8 @@
<string name="room_join_rules_public_by_you">You made the room public to whoever knows the link.</string>
<string name="room_join_rules_invite">%1$s made the room invite only.</string>
<string name="room_join_rules_invite_by_you">You made the room invite only.</string>
<string name="direct_room_join_rules_invite">%1$s made this invite only.</string>
<string name="direct_room_join_rules_invite_by_you">You made this invite only.</string>
<string name="timeline_unread_messages">Unread messages</string>
<string name="login_splash_title">It\'s your conversation. Own it.</string>
@@ -2113,12 +2119,15 @@
<string name="verification_request_waiting_for">Waiting for %s…</string>
<string name="verification_request_alert_description">For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.</string>
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
<string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string>
<string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
<string name="direct_room_profile_encrypted_subtitle">Messages here are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
<string name="room_profile_section_security">Security</string>
<string name="room_profile_section_security_learn_more">Learn more</string>
<string name="room_profile_section_more">More</string>
<string name="room_profile_section_admin">Admin Actions</string>
<string name="room_profile_section_more_settings">Room settings</string>
<string name="direct_room_profile_section_more_settings">Settings</string>
<string name="room_profile_section_more_notifications">Notifications</string>
<plurals name="room_profile_section_more_member_list">
<item quantity="one">"One person"</item>
@@ -2126,6 +2135,7 @@
</plurals>
<string name="room_profile_section_more_uploads">Uploads</string>
<string name="room_profile_section_more_leave">Leave Room</string>
<string name="direct_room_profile_section_more_leave">Leave</string>
<string name="room_profile_leaving_room">"Leaving the room…"</string>
<string name="room_member_power_level_admins">Admins</string>
@@ -2355,11 +2365,14 @@
<string name="encryption_enabled">Encryption enabled</string>
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
<string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string>
<string name="encryption_not_enabled">Encryption not enabled</string>
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
<string name="room_created_summary_item">%s created and configured the room.</string>
<string name="room_created_summary_item_by_you">You created and configured the room.</string>
<string name="direct_room_created_summary_item">%s joined.</string>
<string name="direct_room_created_summary_item_by_you">You joined.</string>
<string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string>
<string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string>
@@ -2434,6 +2447,15 @@
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
<string name="secure_backup_reset_all">Reset everything</string>
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
<plurals name="secure_backup_reset_devices_you_can_verify">
<item quantity="one">Show the device you can verify with now</item>
<item quantity="other">Show %d devices you can verify with now</item>
</plurals>
<string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string>

View File

@@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LoginContainer">
<item name="android:paddingTop">32dp</item>
<item name="android:paddingBottom">32dp</item>
<item name="android:paddingStart">36dp</item>
<item name="android:paddingEnd">36dp</item>
</style>
<item name="loginLogo" type="id" />
<item name="loginFormScrollView" type="id" />
<item name="loginFormContainer" type="id" />

View File

@@ -16,6 +16,12 @@
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:key="SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
android:title="@string/settings_labs_show_complete_history_in_encrypted_room" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"

View File

@@ -39,13 +39,6 @@
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
android:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_MERGE_E2E_ERRORS"
android:title="@string/labs_merge_e2e_in_timeline" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"