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

Compare commits

..

74 Commits

Author SHA1 Message Date
Valere
192b0ed3fb change way to compute local echo 2020-10-08 14:37:12 +02:00
Valere
c909fc50a2 Perf Tracing server support 2020-10-05 18:44:42 +02:00
ganfra
5cf1175d7f Merge branch 'develop' into feature/performance_tracking 2020-10-01 17:29:22 +02:00
ganfra
7be93e03ba Profiler: clean code 2020-10-01 17:20:51 +02:00
ganfra
98b86e977f Profile send: add one more stage 2020-10-01 16:38:14 +02:00
ganfra
3cb2873f93 Add logs for timeline build in debug 2020-10-01 15:33:57 +02:00
ganfra
bb7cd409e6 Profiler: make it generic so you can use it as you want. 2020-10-01 15:33:48 +02:00
Benoit Marty
28b039fde3 Update CHANGES.md with: Updates Gradle Wrapper from 5.6.4 to 6.6.1. (#2193) 2020-10-01 14:47:35 +02:00
Benoit Marty
762bfc4fcc Merge pull request #2193 from vector-im/gradlew-update-6.6.1
Updates Gradle Wrapper from 5.6.4 to 6.6.1.
2020-10-01 14:44:55 +02:00
gradle-update-robot
39532fc2aa Update Gradle Wrapper from 5.6.4 to 6.6.1.
Signed-off-by: gradle-update-robot <gradle-update-robot@regolo.cc>
2020-10-01 00:53:05 +00:00
ganfra
cef191f1a5 Performance tracking: add more stages in encryption 2020-09-30 19:20:38 +02:00
Valere
b6b73f2361 Merge pull request #2187 from vector-im/feature/forgot_pass_reset_all_4S
Feature/forgot pass reset all 4 s
2020-09-30 18:22:48 +02:00
Benoit Marty
37d6a9b722 ktlint 2020-09-30 18:20:42 +02:00
Benoit Marty
2a2187196f Merge pull request #2190 from vector-im/feature/utd_pagination
Feature/utd pagination
2020-09-30 18:19:00 +02:00
Benoit Marty
881ebff015 Changelog and small improvement 2020-09-30 18:11:30 +02:00
ganfra
f0272fd283 UTD pagination: clean up and add developer settings to enable complete history 2020-09-30 17:56:50 +02:00
ganfra
0a0330a48c UTD : when reaching UTD and invite state event, stop back pagination 2020-09-30 17:54:16 +02:00
Benoit Marty
c09d308df8 Merge pull request #2176 from vector-im/feature/fix_dm_wordings
Wording differentiation for DM "chat" instead of "room"
2020-09-30 17:50:58 +02:00
Benoit Marty
482bb51640 More cleanup 2020-09-30 17:50:36 +02:00
Benoit Marty
ee56307ccc No warning when cancelling the Reset of 4s 2020-09-30 16:47:34 +02:00
Benoit Marty
18950a6b46 Some cleanup 2020-09-30 16:36:17 +02:00
Valere
a4e163885d Better rotation support 2020-09-30 16:17:28 +02:00
Benoit Marty
108a31eca3 Avoid long lines 2020-09-30 15:31:33 +02:00
Benoit Marty
7f26dbe260 Fix compil issue 2020-09-30 15:23:27 +02:00
Benoit Marty
5e2f65ab7a Split long lines 2020-09-30 12:46:28 +02:00
Benoit Marty
c5459cdde4 Use RoomSummaryHolder if available 2020-09-30 12:39:36 +02:00
Benoit Marty
f58829130a More cleanup 2020-09-30 12:30:21 +02:00
Benoit Marty
42a3a64b0d Better way to get direct state of the room (using view state) 2020-09-30 12:26:25 +02:00
Benoit Marty
1986de36a6 Better wording for DM creation (note: this event is hidden in the timeline by default) 2020-09-30 12:12:44 +02:00
Benoit Marty
84202adc5b Add braces to multiline if else 2020-09-30 11:54:23 +02:00
Benoit Marty
07446d2d41 Embedded if 2020-09-30 11:50:07 +02:00
Benoit Marty
14162ecaa0 Add missing case 2020-09-30 11:47:37 +02:00
Benoit Marty
2f054cd438 Fix mistake 2020-09-30 11:45:21 +02:00
Benoit Marty
b5311aa3df Simple code 2020-09-30 11:42:07 +02:00
Valere
3642ca5b4a cleaning 2020-09-30 11:05:06 +02:00
Valere
2908a5d345 Added first ui bootstrap test 2020-09-30 10:13:59 +02:00
Benoit Marty
d225fb7df0 Update Changelog 2020-09-30 10:08:26 +02:00
Onuray Sahin
5d18a7cc82 Lint fixes. 2020-09-30 10:07:20 +02:00
Onuray Sahin
07976988d9 Use RoomSummary to check if the room is direct. 2020-09-30 10:07:20 +02:00
Onuray Sahin
2a96b2c68e Differentiate wordings for the room profile screen. 2020-09-30 10:07:20 +02:00
Onuray Sahin
3a9e6fa97f Changelog added. 2020-09-30 10:07:20 +02:00
Onuray Sahin
24fcb3f58f Differentiate wordings for direct rooms. 2020-09-30 10:07:20 +02:00
Benoit Marty
5beaa93437 Merge pull request #2162 from gradle-update/develop
Add workflow for Update Gradle Wrapper Action.
2020-09-30 09:49:00 +02:00
Benoit Marty
415fb3a432 Merge pull request #2186 from vector-im/feature/bma_intent
Finish what has been started on #1376: use Intent.ACTION_GET_CONTENT …
2020-09-30 09:48:10 +02:00
ganfra
2ab2c5c94b Performance tracker: start implementing send tracking 2020-09-29 21:07:58 +02:00
Valere
1e3bdd6a2e Fix test 2020-09-29 18:00:15 +02:00
Cristian Greco
68907ff6c0 Merge remote-tracking branch 'upstream/develop' into develop 2020-09-29 17:40:55 +02:00
Valere
9f26d015ba Update change log
Fixes #2052
2020-09-29 17:07:56 +02:00
Valere
c20517599e Add option to reset 4S if lost pass/key 2020-09-29 17:05:29 +02:00
Benoit Marty
0bb75eed1f Finish what has been started on #1376: use Intent.ACTION_GET_CONTENT instead of Intent.ACTION_OPEN_DOCUMENT for other pickers 2020-09-29 16:41:55 +02:00
Benoit Marty
2b90f1395f Merge pull request #1376 from dkanada/patch-1
Fix gallery intent for certain apps
2020-09-29 16:29:21 +02:00
Benoit Marty
51f225056c Merge branch 'develop' into patch-1 2020-09-29 16:28:58 +02:00
Benoit Marty
7a494db40b Merge pull request #2167 from vector-im/feature/ui_test
Feature/ui test
2020-09-29 16:13:54 +02:00
Benoit Marty
487bbe42a9 Merge branch 'develop' into feature/ui_test 2020-09-29 16:13:44 +02:00
Benoit Marty
ab74f6c1a8 Merge pull request #2165 from vector-im/feature/timeline_scroll_opti
Feature/timeline scroll opti
2020-09-29 16:11:57 +02:00
Benoit Marty
2def7f3910 PR Review 2020-09-29 16:10:54 +02:00
ganfra
11a4704161 Clean files and update CHANGES 2020-09-29 15:42:48 +02:00
ganfra
8bc0afa75e Timeline: add glide preloading 2020-09-29 15:42:21 +02:00
ganfra
3f5b1083f3 Timeline: add a prefetch backward item 2020-09-29 15:42:21 +02:00
Benoit Marty
435724ffa9 Merge pull request #2182 from vector-im/feature/bma_hs_diag
Create a script to help getting public information form any homeserver
2020-09-29 15:39:15 +02:00
Benoit Marty
b14d22550b PR Review
Cleanup and Add command line to run the UI tests
2020-09-29 15:12:25 +02:00
Valere
f79784bc8c Stabilisation
Hide keyboard before entering text
2020-09-29 12:51:27 +02:00
Valere
6ac401db9b Doc + change log 2020-09-29 12:51:27 +02:00
Valere
bc2c345e21 First automated UI tests 2020-09-29 12:51:27 +02:00
Benoit Marty
577f0e0d9a Create a script to help getting public information form any homeserver 2020-09-29 11:38:19 +02:00
Benoit Marty
a3570a69dd Merge pull request #2181 from vector-im/feature/bma_pin_fix
PIN code: request PIN code if phone has been locked
2020-09-29 10:03:02 +02:00
Benoit Marty
77f06b962d PIN code: request PIN code if phone has been locked 2020-09-28 16:57:36 +02:00
Benoit Marty
d0ec5a13f3 Merge pull request #2166 from vector-im/feature/bma_splash_quick_fix
Fix Splash screen layout, especially on small screens
2020-09-28 13:52:03 +02:00
Benoit Marty
1ed0ef0948 Disable animation on title 2020-09-26 12:02:22 +02:00
Benoit Marty
21f1848499 Fix Splash screen layout, especially on small screens 2020-09-26 11:30:13 +02:00
Benoit Marty
6958d114a9 Version++ 2020-09-25 14:09:25 +02:00
Benoit Marty
1bc42959d0 Merge branch 'release/1.0.8' into develop 2020-09-25 14:08:24 +02:00
Cristian Greco
62f620f79b Add workflow for Update Gradle Wrapper Action.
This action keeps Gradle Wrapper up-to-date to the latest release. It
will run every day at midnight (UTC) and create a pull request if a new
Gradle version is available. The updated Wrapper script is validated
(with checksum verification) during the update process, and the Wrapper
is setup so that it will validate the Gradle binary itself on first run
of the new version.

Signed-off-by: Cristian Greco <cristian@regolo.cc>
2020-09-24 11:19:28 +02:00
dkanada
3442ebc1c3 improve gallery intent for certain apps 2020-09-04 21:01:49 +09:00
111 changed files with 3249 additions and 672 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,3 +1,33 @@
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)
===================================================

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

@@ -56,11 +56,19 @@ android {
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
// Set to BODY instead of NONE to enable logging
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level." + project.property("vector.httpLogLevel")
buildConfigField "String", "PERF_TRACING_SERVER", project.property("vector.perfTracingServer")
buildConfigField "String", "PERF_TRACING_SERVER_TOKEN", project.property("vector.perfTracingServerToken")
buildConfigField "String", "PERF_TRACING_SERVER_USER", project.property("vector.perfTracingServerUser")
}
release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
buildConfigField "String", "PERF_TRACING_SERVER", ""
buildConfigField "String", "PERF_TRACING_SERVER_TOKEN", ""
buildConfigField "String", "PERF_TRACING_SERVER_USER", ""
}
}
@@ -169,6 +177,9 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
debugImplementation 'com.nikitakozlov.pury:pury:1.1.0'
releaseImplementation 'com.nikitakozlov.pury:pury-no-op:1.1.0'
// Bus
implementation 'org.greenrobot:eventbus:3.1.1'

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

@@ -21,11 +21,13 @@ import android.content.Context
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import androidx.work.WorkManager
import com.nikitakozlov.pury.Pury
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.util.profiling.PerfServerPlugin
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.UserAgentHolder
@@ -48,6 +50,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var perfServerPlugin: PerfServerPlugin
init {
Monarchy.init(context)
@@ -56,6 +59,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
}
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
if (!BuildConfig.PERF_TRACING_SERVER.isNullOrEmpty()) {
Pury.addPlugin("per-server", perfServerPlugin)
}
}
fun getUserAgent() = userAgentHolder.userAgent

View File

@@ -112,7 +112,8 @@ interface CryptoService {
fun isRoomEncrypted(roomId: String): Boolean
fun encryptEventContent(eventContent: Content,
fun encryptEventContent(eventId: String,
eventContent: Content,
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>)

View File

@@ -0,0 +1,37 @@
/*
* 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 org.matrix.android.sdk.api.session.room.send
import org.matrix.android.sdk.api.util.profiling.BaseProfiler
object SendPerformanceProfiler: BaseProfiler<SendPerformanceProfiler.Stages>() {
enum class Stages() {
// LOCAL_ECHO,
ENCRYPT_WORKER,
ENCRYPT_GET_USERS,
ENCRYPT_SET_ROOM_ENCRYPTION,
ENCRYPT_MEGOLM_SHARE_KEYS,
ENCRYPT_MEGOLM_ENCRYPT,
SEND_WORKER,
GET_UP_TO_DATE_ECHO,
SEND_REQUEST,
RECEIVED_IN_SYNC
}
override val name = "SEND_PROFILER"
}

View File

@@ -34,9 +34,10 @@ interface SendService {
* Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead.
* @param eventType the type of the event
* @param content the optional body as a json dict.
* @param onBuiltEvent lambda to react to the event creation
* @return a [Cancelable]
*/
fun sendEvent(eventType: String, content: Content?): Cancelable
fun sendEvent(eventType: String, content: Content?, onBuiltEvent: ((Event) -> Unit)? = null): Cancelable
/**
* Method to send a text message asynchronously.
@@ -47,7 +48,12 @@ interface SendService {
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @return a [Cancelable]
*/
fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
fun sendTextMessage(
text: CharSequence,
msgType: String = MessageType.MSGTYPE_TEXT,
autoMarkdown: Boolean = false,
onBuiltEvent: ((Event) -> Unit)? = null
): Cancelable
/**
* Method to send a text message with a formatted body.
@@ -56,7 +62,12 @@ interface SendService {
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @return a [Cancelable]
*/
fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
fun sendFormattedTextMessage(
text: String,
formattedText: String,
msgType: String = MessageType.MSGTYPE_TEXT,
onBuiltEvent: ((Event) -> Unit)? = null
): Cancelable
/**
* Method to send a media asynchronously.

View File

@@ -0,0 +1,98 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import com.nikitakozlov.pury.Pury
private const val ROOT_STAGE = "ROOT_STAGE"
/**
* Base class for profilers, it's implementing Pury start and stop profiling.
* Order of STAGE in enum matters.
*/
abstract class BaseProfiler<STAGE : Enum<STAGE>> {
private val currentProfilers = ArrayList<String>()
abstract val name: String
/**
* Use to start the profiling process. It's necessary to call this method before any other method.
* You should always call stop profiling with the same key param when you want to stop profiling.
*
* @param key unique identifier of the profiling.
*/
fun startProfiling(key: String) {
if (currentProfilers.contains(key)) {
return
}
currentProfilers.add(key)
Pury.startProfiling(profilerName(key), ROOT_STAGE, 0, 1)
}
/**
* Use to stop the profiling process. This will dispatch information to the configured pury plugins.
*
* @param key unique identifier of the profiling.
*/
fun stopProfiling(key: String) {
if (!currentProfilers.contains(key)) {
return
}
Pury.stopProfiling(profilerName(key), ROOT_STAGE, 1)
currentProfilers.remove(key)
}
/**
* Use to profile a block of code. Internally it will call start and stop stage.
*
* @param key unique identifier of the profiling.
* @param stage the current stage to mark
*/
fun profileBlock(key: String, stage: STAGE, block: (() -> Unit)? = null) {
startStage(key, stage)
block?.invoke()
stopStage(key, stage)
}
/**
* Use to add a new stage to profile inside the root profiling.
* You should have called startProfiling with the same key before.
* You should also call stopStage with same key and same stage to mark the end of this stage.
*
* @param key unique identifier of the profiling.
* @param stage the current stage to profile
*/
fun startStage(key: String, stage: STAGE) {
if (!currentProfilers.contains(key)) return
Pury.startProfiling(profilerName(key), stage.name, stage.ordinal + 1, 1)
}
/**
* Use to finish the profiling of a stage.
* You should have called startStage with the same key and same stage before.
*
* @param key unique identifier of the profiling.
* @param stage the current stage to profile
*/
fun stopStage(key: String, stage: STAGE) {
if (!currentProfilers.contains(key)) return
Pury.stopProfiling(profilerName(key), stage.name, 1)
}
private fun profilerName(key: String) = "${name}_$key"
}

View File

@@ -0,0 +1,27 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface PerServerAPI {
@POST("api/result")
fun postReport(@Body profileResult: ProfileReport): Call<Unit>
}

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 org.matrix.android.sdk.api.util.profiling
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
interface ProfileResult {
val name: String?
val depth: Int?
val nestedResults: List<ProfileResult>?
}
@JsonClass(generateAdapter = true)
data class SingleProfileResultRest(
override val name: String? = null,
override val depth: Int? = 0,
override val nestedResults: List<SingleProfileResultRest>? = null,
val startTime: Long? = 0L,
val execTime: Long? = 0L
) : ProfileResult
@JsonClass(generateAdapter = true)
data class ProfileReport(
@Json(name = "user")
val user: String? = null,
@Json(name = "device")
val device: String? = null,
@Json(name = "id")
val id: String? = null,
@Json(name = "rootProfileResults")
val rootProfileResult: SingleProfileResultRest? = null,
@Json(name = "tag")
val tag: String? = null
)

View File

@@ -0,0 +1,57 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.network.HttpHeaders
import org.matrix.android.sdk.internal.network.RetrofitFactory
@Module
internal abstract class PerfModule {
@Module
companion object {
@Provides
@JvmStatic
fun providesPrefServerAPI(@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory): PerServerAPI {
val client = okHttpClient.get().newBuilder()
.addInterceptor { chain ->
var request = chain.request()
val token = BuildConfig.PERF_TRACING_SERVER_TOKEN
val newRequestBuilder = request.newBuilder()
newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token")
request = newRequestBuilder.build()
chain.proceed(request)
}
.hostnameVerifier(OkHostnameVerifier)
.build()
return retrofitFactory.create(client, BuildConfig.PERF_TRACING_SERVER).create(PerServerAPI::class.java)
}
}
@Binds
abstract fun bindPublishTask(task: DefaultPublishPerfTask): PublishPerfTask
}

View File

@@ -0,0 +1,62 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import android.os.Build
import com.nikitakozlov.pury.Plugin
import com.nikitakozlov.pury.profile.ProfilerId
import com.nikitakozlov.pury.result.model.ProfileResult
import com.nikitakozlov.pury.result.model.SingleProfileResult
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import javax.inject.Inject
internal class PerfServerPlugin @Inject constructor(
private val publishPerfTask: PublishPerfTask,
private val taskExecutor: TaskExecutor
) : Plugin {
override fun handleResult(result: ProfileResult?, profilerId: ProfilerId?) {
val report = ProfileReport(
user = BuildConfig.PERF_TRACING_SERVER_USER,
device = Build.DEVICE,
id = profilerId?.profilerName,
rootProfileResult = (result as? SingleProfileResult)?.let { ResultMapper.map(it) },
tag = "original"
)
publishPerfTask.configureWith(PublishPerfTask.Params(report))
.executeBy(taskExecutor)
}
object ResultMapper {
private const val MS_TO_NS = 1000000
fun map(singleProfileResult: SingleProfileResult): SingleProfileResultRest {
return SingleProfileResultRest(
name = singleProfileResult.stageName,
depth = singleProfileResult.depth,
execTime = singleProfileResult.execTime / MS_TO_NS,
startTime = singleProfileResult.startTime / MS_TO_NS,
nestedResults = singleProfileResult.nestedResults.filterIsInstance<SingleProfileResult>().map {
map(it)
}
)
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import com.nikitakozlov.pury.Logger
import com.nikitakozlov.pury.Pury
import timber.log.Timber
object ProfilingConfiguration {
init {
Pury.setLogger(object : Logger {
override fun result(tag: String, message: String) {
Timber.tag(tag)
Timber.v(message)
}
override fun warning(tag: String, message: String) {
Timber.tag(tag)
Timber.w(message)
}
override fun error(tag: String, message: String) {
Timber.tag(tag)
Timber.e(message)
}
})
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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 org.matrix.android.sdk.api.util.profiling
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface PublishPerfTask : Task<PublishPerfTask.Params, Unit> {
data class Params(
val report: ProfileReport
)
}
internal class DefaultPublishPerfTask @Inject constructor(
private val perfAPI: PerServerAPI
) : PublishPerfTask {
override suspend fun execute(params: PublishPerfTask.Params) {
return executeRequest(null) {
apiCall = perfAPI.postReport(params.report)
}
}
}

View File

@@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@@ -653,12 +654,14 @@ internal class DefaultCryptoService @Inject constructor(
/**
* Encrypt an event content according to the configuration of the room.
*
* @param eventId the identifier of the event
* @param eventContent the content of the event.
* @param eventType the type of the event.
* @param roomId the room identifier the event will be sent.
* @param callback the asynchronous callback
*/
override fun encryptEventContent(eventContent: Content,
override fun encryptEventContent(eventId: String,
eventContent: Content,
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
@@ -667,7 +670,12 @@ internal class DefaultCryptoService @Inject constructor(
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
// internalStart(false)
// }
val userIds = getRoomUserIds(roomId)
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_GET_USERS)
val userIds = getRoomUserIds(roomId)
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_GET_USERS)
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_SET_ROOM_ENCRYPTION)
var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
val algorithm = getEncryptionAlgorithm(roomId)
@@ -677,12 +685,13 @@ internal class DefaultCryptoService @Inject constructor(
}
}
}
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_SET_ROOM_ENCRYPTION)
val safeAlgorithm = alg
if (safeAlgorithm != null) {
val t0 = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent() starts")
runCatching {
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
val content = safeAlgorithm.encryptEventContent(eventId, eventContent, eventType, userIds)
Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}.foldToCallback(callback)
@@ -803,17 +812,17 @@ internal class DefaultCryptoService @Inject constructor(
onRoomKeyEvent(event)
}
EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> {
EventType.ROOM_KEY_REQUEST -> {
// save audit trail
cryptoStore.saveGossipingEvent(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
}
EventType.SEND_SECRET -> {
EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
EventType.ROOM_KEY_WITHHELD -> {
onKeyWithHeldReceived(event)
}
else -> {
@@ -892,7 +901,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
true
}
@@ -1351,9 +1360,9 @@ internal class DefaultCryptoService @Inject constructor(
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
}
/* ==========================================================================================
* For test only
* ========================================================================================== */
/* ==========================================================================================
* For test only
* ========================================================================================== */
@VisibleForTesting
val cryptoStoreForTesting = cryptoStore

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

@@ -33,7 +33,7 @@ internal interface IMXEncrypting {
* @param userIds the room members the event will be sent to.
* @return the encrypted content
*/
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
suspend fun encryptEventContent(eventId: String, eventContent: Content, eventType: String, userIds: List<String>): Content
/**
* In Megolm, each recipient maintains a record of the ratchet value which allows

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -71,17 +72,20 @@ internal class MXMegolmEncryption(
private var sessionRotationPeriodMsgs: Int = 100
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
override suspend fun encryptEventContent(eventContent: Content,
override suspend fun encryptEventContent(eventId: String,
eventContent: Content,
eventType: String,
userIds: List<String>): Content {
val ts = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds)
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
val outboundSession = ensureOutboundSession(devices.allowedDevices)
val outboundSession = ensureOutboundSession(eventId, devices.allowedDevices)
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_MEGOLM_ENCRYPT)
return encryptContent(outboundSession, eventType, eventContent)
.also {
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_MEGOLM_ENCRYPT)
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
}
}
@@ -128,7 +132,7 @@ internal class MXMegolmEncryption(
*
* @param devicesInRoom the devices list
*/
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
private suspend fun ensureOutboundSession(eventId: String, devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
Timber.v("## CRYPTO | ensureOutboundSession start")
var session = outboundSession
if (session == null
@@ -152,7 +156,9 @@ internal class MXMegolmEncryption(
}
}
}
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_MEGOLM_SHARE_KEYS)
shareKey(safeSession, shareMap)
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_MEGOLM_SHARE_KEYS)
return safeSession
}
@@ -307,6 +313,7 @@ internal class MXMegolmEncryption(
// Get canonical Json from
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString)
val map = HashMap<String, Any>()

View File

@@ -38,7 +38,7 @@ internal class MXOlmEncryption(
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction)
: IMXEncrypting {
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
override suspend fun encryptEventContent(eventId: String, eventContent: Content, eventType: String, userIds: List<String>): Content {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up

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

@@ -54,7 +54,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
// try {
awaitCallback<MXEncryptEventContentResult> {
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
params.crypto.encryptEventContent(localEvent.eventId, localMutableContent, localEvent.type, params.roomId, it)
}.let { result ->
val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep ->

View File

@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.util.profiling.PerfModule
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule
import org.matrix.android.sdk.internal.auth.SessionParamsStore
@@ -44,6 +45,7 @@ import java.io.File
NetworkModule::class,
AuthModule::class,
RawModule::class,
PerfModule::class,
NoOpTestModule::class
])
@MatrixScope

View File

@@ -21,11 +21,11 @@ import android.content.Context
import android.content.res.Resources
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import org.matrix.olm.OlmManager
import java.io.File
import java.util.concurrent.Executors

View File

@@ -22,11 +22,13 @@ import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import com.nikitakozlov.pury.annotations.StartProfiling
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage
@@ -44,7 +46,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -81,14 +82,21 @@ internal class DefaultSendService @AssistedInject constructor(
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendEvent(eventType: String, content: JsonDict?): Cancelable {
override fun sendEvent(eventType: String, content: Content?, onBuiltEvent: ((Event) -> Unit)?): Cancelable {
return localEchoEventFactory.createEvent(roomId, eventType, content)
.also { onBuiltEvent?.invoke(it) }
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
override fun sendTextMessage(
text: CharSequence,
msgType: String,
autoMarkdown: Boolean,
onBuiltEvent: ((Event) -> Unit)?
): Cancelable {
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
.also { onBuiltEvent?.invoke(it) }
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
@@ -106,8 +114,14 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
override fun sendFormattedTextMessage(
text: String,
formattedText: String,
msgType: String,
onBuiltEvent: ((Event) -> Unit)?
): Cancelable {
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
.also { onBuiltEvent?.invoke(it) }
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
@@ -138,6 +152,7 @@ internal class DefaultSendService @AssistedInject constructor(
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
}
@StartProfiling(profilerName = "Sending", stageName = "Send service", stageOrder = 0)
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)

View File

@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
@@ -65,7 +66,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
override suspend fun doSafeWork(params: Params): Result {
Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.GET_UP_TO_DATE_ECHO)
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
SendPerformanceProfiler.stopStage(params.eventId, SendPerformanceProfiler.Stages.GET_UP_TO_DATE_ECHO)
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.ENCRYPT_WORKER)
if (localEvent?.eventId == null) {
return Result.success()
}
@@ -86,7 +91,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
var result: MXEncryptEventContentResult? = null
try {
result = awaitCallback {
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
crypto.encryptEventContent(localEvent.eventId, localMutableContent, localEvent.type, localEvent.roomId!!, it)
}
} catch (throwable: Throwable) {
error = throwable
@@ -122,7 +127,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
localEcho.setDecryptionResult(it)
}
}
SendPerformanceProfiler.stopStage(params.eventId, SendPerformanceProfiler.Stages.ENCRYPT_WORKER)
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} else {

View File

@@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.isReply
@@ -325,6 +326,7 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createEvent(roomId: String, type: String, content: Content?): Event {
val localId = LocalEcho.createLocalEchoId()
SendPerformanceProfiler.startProfiling(localId)
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
@@ -433,10 +435,10 @@ internal class LocalEchoEventFactory @Inject constructor(
TextContent(content.body, formattedText)
}
}
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
else -> return TextContent(content?.body ?: "")
}
}

View File

@@ -25,6 +25,7 @@ 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.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.database.RealmSessionProvider
@@ -62,6 +63,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
if (event.eventId == null) {
throw IllegalStateException("You should have set an eventId for your event")
}
// SendPerformanceProfiler.startStage(event.eventId, SendPerformanceProfiler.Stages.LOCAL_ECHO)
val timelineEventEntity = realmSessionProvider.withRealm { realm ->
val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis())
val roomMemberHelper = RoomMemberHelper(realm, roomId)
@@ -86,6 +88,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@asyncTransaction
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.updateSendingInformation(realm, roomId)
//SendPerformanceProfiler.stopStage(event.eventId, SendPerformanceProfiler.Stages.LOCAL_ECHO)
}
}

View File

@@ -37,6 +37,7 @@ internal class RoomEventSender @Inject constructor(
@SessionId private val sessionId: String,
private val cryptoService: CryptoService
) {
fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")

View File

@@ -24,6 +24,7 @@ import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
@@ -62,6 +63,7 @@ internal class SendEventWorker(context: Context,
}
override suspend fun doSafeWork(params: Params): Result {
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
val event = localEchoRepository.getUpToDateEcho(params.eventId)
if (event?.eventId == null || event.roomId == null) {
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
@@ -87,6 +89,7 @@ internal class SendEventWorker(context: Context,
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
return try {
sendEvent(event.eventId, event.roomId, event.type, event.content)
SendPerformanceProfiler.stopStage(event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
Result.success()
} catch (exception: Throwable) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
@@ -106,9 +109,11 @@ internal class SendEventWorker(context: Context,
private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
localEchoRepository.updateSendState(eventId, SendState.SENDING)
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.SEND_REQUEST)
executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send(eventId, roomId, type, content)
}
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.SEND_REQUEST)
localEchoRepository.updateSendState(eventId, SendState.SENT)
}
}

View File

@@ -65,6 +65,7 @@ import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
import io.realm.Realm
import io.realm.kotlin.createObject
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import timber.log.Timber
import javax.inject.Inject
@@ -333,6 +334,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
event.unsignedData?.transactionId?.also {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
if (sendingEventEntity != null) {
SendPerformanceProfiler.profileBlock(it, SendPerformanceProfiler.Stages.RECEIVED_IN_SYNC)
Timber.v("Remove local echo for tx:$it")
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) {

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===78
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")
@@ -316,6 +334,9 @@ dependencies {
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
debugImplementation 'com.nikitakozlov.pury:pury:1.1.0'
releaseImplementation 'com.nikitakozlov.pury:pury-no-op:1.1.0'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
@@ -326,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'
@@ -421,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

@@ -94,6 +94,7 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@@ -551,7 +552,9 @@ class RoomDetailViewModel @AssistedInject constructor(
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) {
SendPerformanceProfiler.startProfiling(it.eventId!!)
}
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
@@ -890,6 +893,9 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
viewModelScope.launch(Dispatchers.Default) {
if (action.event.root.sendState.isSent()) { // ignore pending/local events
action.event.root.unsignedData?.transactionId?.also {
SendPerformanceProfiler.stopProfiling(it)
}
visibleEventsObservable.accept(action)
}
// We need to update this with the related m.replace also (to move read receipt)

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

@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.BuildConfig
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_
@@ -50,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 {
@@ -113,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
@@ -163,6 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
init {
isDebugLoggingEnabled = BuildConfig.DEBUG
addInterceptor(this)
requestModelBuild()
}
@@ -191,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) {
@@ -241,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
@@ -301,6 +341,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
hasUTD = false
hasReachedInvite = false
if (modelCache.isEmpty()) {
return
}
@@ -316,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,
@@ -346,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
*/
@@ -355,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

@@ -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 {

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