mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
162 Commits
v1.0.7
...
feature/pe
Author | SHA1 | Date | |
---|---|---|---|
|
192b0ed3fb | ||
|
c909fc50a2 | ||
|
5cf1175d7f | ||
|
7be93e03ba | ||
|
98b86e977f | ||
|
3cb2873f93 | ||
|
bb7cd409e6 | ||
|
28b039fde3 | ||
|
762bfc4fcc | ||
|
39532fc2aa | ||
|
cef191f1a5 | ||
|
b6b73f2361 | ||
|
37d6a9b722 | ||
|
2a2187196f | ||
|
881ebff015 | ||
|
f0272fd283 | ||
|
0a0330a48c | ||
|
c09d308df8 | ||
|
482bb51640 | ||
|
ee56307ccc | ||
|
18950a6b46 | ||
|
a4e163885d | ||
|
108a31eca3 | ||
|
7f26dbe260 | ||
|
5e2f65ab7a | ||
|
c5459cdde4 | ||
|
f58829130a | ||
|
42a3a64b0d | ||
|
1986de36a6 | ||
|
84202adc5b | ||
|
07446d2d41 | ||
|
14162ecaa0 | ||
|
2f054cd438 | ||
|
b5311aa3df | ||
|
3642ca5b4a | ||
|
2908a5d345 | ||
|
d225fb7df0 | ||
|
5d18a7cc82 | ||
|
07976988d9 | ||
|
2a96b2c68e | ||
|
3a9e6fa97f | ||
|
24fcb3f58f | ||
|
5beaa93437 | ||
|
415fb3a432 | ||
|
2ab2c5c94b | ||
|
1e3bdd6a2e | ||
|
68907ff6c0 | ||
|
9f26d015ba | ||
|
c20517599e | ||
|
0bb75eed1f | ||
|
2b90f1395f | ||
|
51f225056c | ||
|
7a494db40b | ||
|
487bbe42a9 | ||
|
ab74f6c1a8 | ||
|
2def7f3910 | ||
|
11a4704161 | ||
|
8bc0afa75e | ||
|
3f5b1083f3 | ||
|
435724ffa9 | ||
|
b14d22550b | ||
|
f79784bc8c | ||
|
6ac401db9b | ||
|
bc2c345e21 | ||
|
577f0e0d9a | ||
|
a3570a69dd | ||
|
77f06b962d | ||
|
d0ec5a13f3 | ||
|
1ed0ef0948 | ||
|
21f1848499 | ||
|
6958d114a9 | ||
|
1bc42959d0 | ||
|
22cfb64348 | ||
|
056405939b | ||
|
2dcaabe4c4 | ||
|
bbd86661a4 | ||
|
d684c11c65 | ||
|
a2fb6d5664 | ||
|
31029e90cc | ||
|
2e95c78f4e | ||
|
62f620f79b | ||
|
f271968238 | ||
|
27d3a36c1b | ||
|
dfa0308db6 | ||
|
299bcc2bc7 | ||
|
03c66315cb | ||
|
9f154748ae | ||
|
5eb66c4617 | ||
|
44b2673848 | ||
|
a6cf2b0685 | ||
|
b8c350488b | ||
|
fb74628aa8 | ||
|
fdedfc954c | ||
|
b8cbafa75d | ||
|
9ab053d702 | ||
|
6e2c733319 | ||
|
ad2191a76e | ||
|
ecc189aeac | ||
|
34d2c3d391 | ||
|
1699a57850 | ||
|
ea9b99b38b | ||
|
8edecf5937 | ||
|
cede7b1dc1 | ||
|
1464f5aa02 | ||
|
5ab7ec0bc8 | ||
|
7f85331448 | ||
|
7c063972ac | ||
|
bd72c0ca8d | ||
|
91fe308113 | ||
|
78a76a8038 | ||
|
759974d9a8 | ||
|
00a3e802fc | ||
|
160c1b49a1 | ||
|
5b1737ae46 | ||
|
fee7701d26 | ||
|
1e5122f741 | ||
|
d6ba653c21 | ||
|
51b1d1fa87 | ||
|
4249d9d906 | ||
|
a7e33e9c0a | ||
|
7a2e70ee2d | ||
|
416c153d2e | ||
|
23d911cc2c | ||
|
d868ef0168 | ||
|
48b10e6d08 | ||
|
4d9bd57336 | ||
|
30ab6d5ee7 | ||
|
622faf5471 | ||
|
4d558c5f95 | ||
|
a29ca2ae09 | ||
|
84df86df37 | ||
|
22e03b14d1 | ||
|
d1fed08764 | ||
|
bca24c0198 | ||
|
822c47d15f | ||
|
03b3b82e0a | ||
|
8279191339 | ||
|
6486b9e5cd | ||
|
4d7b0e3e68 | ||
|
66b0e6c68f | ||
|
69a4312613 | ||
|
293f867988 | ||
|
8d2c0dcb48 | ||
|
bdaecc5b2e | ||
|
fa9ff86ab2 | ||
|
f8e3e33e49 | ||
|
73778f8669 | ||
|
cd5a146f1a | ||
|
3c45b84474 | ||
|
d4682e504c | ||
|
2717cca267 | ||
|
28d37f0106 | ||
|
30b51449ce | ||
|
4b66ffcd5b | ||
|
0e85a3d59f | ||
|
72f8c8ef72 | ||
|
b227dc3e5c | ||
|
144d0e56cc | ||
|
d29d1ead9b | ||
|
216138394f | ||
|
719189bd90 | ||
|
3442ebc1c3 |
10
.github/ISSUE_TEMPLATE/matrix-sdk.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/matrix-sdk.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Matrix SDK
|
||||
about: Report issue or ask for a feature regarding the Android Matrix SDK
|
||||
title: "[SDK] "
|
||||
labels: matrix-sdk
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This issue template should be used by third party application maintainers, to report a bug or to request a feature on the SDK module of the application Element Android-->
|
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal file
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal 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
|
58
CHANGES.md
58
CHANGES.md
@@ -1,3 +1,61 @@
|
||||
Changes in Element 1.0.9 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
||||
|
||||
Improvements 🙌:
|
||||
- Wording differentiation for direct rooms (#2176)
|
||||
- PIN code: request PIN code if phone has been locked
|
||||
- Small optimisation of scrolling experience in timeline (#2114)
|
||||
- Allow user to reset cross signing if he has no way to recover (#2052)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Improve support for image/audio/video/file selection with intent changes (#1376)
|
||||
- Fix Splash layout on small screens
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
- Use Update Gradle Wrapper Action
|
||||
- Updates Gradle Wrapper from 5.6.4 to 6.6.1. (#2193)
|
||||
|
||||
Other changes:
|
||||
- Added registration/verification automated UI tests
|
||||
- Create a script to help getting public information form any homeserver
|
||||
|
||||
Changes in Element 1.0.8 (2020-09-25)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Add "show password" in import Megolm keys dialog
|
||||
- Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112)
|
||||
- Better management of requested permissions (#2048)
|
||||
- Add a setting to show timestamp for all messages (#2123)
|
||||
- Use cache for user color
|
||||
- Allow using an outdated homeserver, at user's risk (#1972)
|
||||
- Restore small logo on login screens and fix scrolling issue on those screens
|
||||
- PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Long message cannot be sent/takes infinite time & blocks other messages (#1397)
|
||||
- Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes)
|
||||
- User Verification in DM not working
|
||||
- Manual import of Megolm keys does back up the imported keys
|
||||
- Auto scrolling to the latest message when sending (#2094)
|
||||
- Fix incorrect permission check when creating widgets (#2137)
|
||||
- Pin code: user has to enter pin code twice (#2005)
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- Rename `tryThis` to `tryOrNull`
|
||||
|
||||
Other changes:
|
||||
- Add an advanced action to reset an account data entry
|
||||
|
||||
Changes in Element 1.0.7 (2020-09-17)
|
||||
===================================================
|
||||
|
||||
|
107
docs/ui-tests.md
Normal file
107
docs/ui-tests.md
Normal 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)
|
||||
}
|
||||
```
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#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
53
gradlew
vendored
@@ -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
43
gradlew.bat
vendored
@@ -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
|
||||
|
@@ -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'
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@@ -212,7 +212,7 @@ class UnwedgingTest : InstrumentedTest {
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
// we should get back the key and be able to decrypt
|
||||
val result = tryThis {
|
||||
val result = tryOrNull {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
}
|
||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
||||
|
@@ -20,7 +20,7 @@ import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@@ -227,7 +227,7 @@ class WithHeldTests : InstrumentedTest {
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
|
||||
// try to decrypt and force key request
|
||||
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||
tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||
}
|
||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||
timeLineEvent != null
|
||||
|
@@ -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
|
||||
|
@@ -17,13 +17,11 @@
|
||||
|
||||
package org.matrix.android.sdk.api.auth.data
|
||||
|
||||
// Either a list of supported login types, or an error if the homeserver is outdated
|
||||
sealed class LoginFlowResult {
|
||||
data class Success(
|
||||
val supportedLoginTypes: List<String>,
|
||||
val isLoginAndRegistrationSupported: Boolean,
|
||||
val homeServerUrl: String
|
||||
val homeServerUrl: String,
|
||||
val isOutdatedHomeserver: Boolean
|
||||
) : LoginFlowResult()
|
||||
|
||||
object OutdatedHomeserver : LoginFlowResult()
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.extensions
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
|
||||
inline fun <A> tryOrNull(message: String? = null, operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.matrix.android.sdk.api.failure
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import java.io.IOException
|
||||
@@ -49,7 +49,7 @@ fun Throwable.isInvalidPassword(): Boolean {
|
||||
*/
|
||||
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
|
||||
return if (this is Failure.OtherServerError && this.httpCode == 401) {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(this.errorBody)
|
||||
|
@@ -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>)
|
||||
|
@@ -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"
|
||||
}
|
@@ -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.
|
||||
|
@@ -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"
|
||||
}
|
@@ -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>
|
||||
}
|
@@ -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
|
||||
)
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -273,16 +273,16 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
|
||||
return if (versions.isSupportedBySdk()) {
|
||||
// Get the login flow
|
||||
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
|
||||
apiCall = authAPI.getLoginFlows()
|
||||
}
|
||||
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
|
||||
} else {
|
||||
// Not supported
|
||||
LoginFlowResult.OutdatedHomeserver
|
||||
// Get the login flow
|
||||
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
|
||||
apiCall = authAPI.getLoginFlows()
|
||||
}
|
||||
return LoginFlowResult.Success(
|
||||
loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
|
||||
versions.isLoginAndRegistrationSupportedBySdk(),
|
||||
homeServerUrl,
|
||||
!versions.isSupportedBySdk()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRegistrationWizard(): RegistrationWizard {
|
||||
|
@@ -18,10 +18,9 @@
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
@@ -32,28 +31,29 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CancelGossipRequestWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<CancelGossipRequestWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
override val sessionId: String,
|
||||
val requestId: String,
|
||||
val recipients: Map<String, List<String>>
|
||||
) {
|
||||
val recipients: Map<String, List<String>>,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams {
|
||||
companion object {
|
||||
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
|
||||
return Params(
|
||||
sessionId = sessionId,
|
||||
requestId = request.requestId,
|
||||
recipients = request.recipients
|
||||
recipients = request.recipients,
|
||||
lastFailureMessage = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,18 +64,11 @@ internal class CancelGossipRequestWorker(context: Context,
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val toDeviceContent = ShareRequestCancellation(
|
||||
@@ -107,13 +100,17 @@ internal class CancelGossipRequestWorker(context: Context,
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
} catch (throwable: Throwable) {
|
||||
return if (throwable.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
|
||||
Result.success(errorOutputData)
|
||||
buildErrorResult(params, throwable.localizedMessage ?: "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -27,10 +27,16 @@ import androidx.lifecycle.LiveData
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
@@ -48,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
|
||||
@@ -102,12 +109,6 @@ import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -345,13 +346,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
// this can throw if no network
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
uploadDeviceKeys()
|
||||
}
|
||||
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
// this can throw if no backup
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
keysBackupService.checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1072,7 +1081,11 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
throw Exception("Error")
|
||||
}
|
||||
|
||||
megolmSessionDataImporter.handle(importedSessions, true, progressListener)
|
||||
megolmSessionDataImporter.handle(
|
||||
megolmSessionsData = importedSessions,
|
||||
fromBackup = false,
|
||||
progressListener = progressListener
|
||||
)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
@@ -1347,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
|
||||
|
@@ -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,
|
||||
|
@@ -18,10 +18,9 @@
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
@@ -34,40 +33,34 @@ import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipRequestWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<SendGossipRequestWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
override val sessionId: String,
|
||||
val keyShareRequest: OutgoingRoomKeyRequest? = null,
|
||||
val secretShareRequest: OutgoingSecretRequest? = null
|
||||
)
|
||||
val secretShareRequest: OutgoingSecretRequest? = null,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val eventType: String
|
||||
@@ -121,7 +114,7 @@ internal class SendGossipRequestWorker(context: Context,
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
return Result.success(errorOutputData).also {
|
||||
return buildErrorResult(params, "Unknown empty gossiping request").also {
|
||||
Timber.e("Unknown empty gossiping request: $params")
|
||||
}
|
||||
}
|
||||
@@ -137,13 +130,17 @@ internal class SendGossipRequestWorker(context: Context,
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
} catch (throwable: Throwable) {
|
||||
return if (throwable.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
|
||||
Result.success(errorOutputData)
|
||||
buildErrorResult(params, throwable.localizedMessage ?: "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -18,10 +18,9 @@
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
@@ -34,22 +33,23 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
override val sessionId: String,
|
||||
val secretValue: String,
|
||||
val request: IncomingSecretShareRequest
|
||||
)
|
||||
val request: IncomingSecretShareRequest,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@@ -58,18 +58,11 @@ internal class SendGossipWorker(context: Context,
|
||||
@Inject lateinit var messageEncrypter: MessageEncrypter
|
||||
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val eventType: String = EventType.SEND_SECRET
|
||||
|
||||
@@ -81,7 +74,7 @@ internal class SendGossipWorker(context: Context,
|
||||
val requestingUserId = params.request.userId ?: ""
|
||||
val requestingDeviceId = params.request.deviceId ?: ""
|
||||
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
|
||||
}
|
||||
@@ -94,7 +87,7 @@ internal class SendGossipWorker(context: Context,
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
return Result.success(errorOutputData).also {
|
||||
return buildErrorResult(params, "no session with this device").also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("no session with this device, probably because there were no one-time keys.")
|
||||
}
|
||||
@@ -130,13 +123,17 @@ internal class SendGossipWorker(context: Context,
|
||||
)
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
} catch (throwable: Throwable) {
|
||||
return if (throwable.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Result.success(errorOutputData)
|
||||
buildErrorResult(params, throwable.localizedMessage ?: "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
||||
* Must be call on the crypto coroutine thread
|
||||
*
|
||||
* @param megolmSessionsData megolm sessions.
|
||||
* @param backUpKeys true to back up them to the homeserver.
|
||||
* @param fromBackup true if the imported keys are already backed up on the server.
|
||||
* @param progressListener the progress listener
|
||||
* @return import room keys result
|
||||
*/
|
||||
|
@@ -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
|
||||
|
@@ -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>()
|
||||
|
@@ -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
|
||||
|
@@ -20,6 +20,10 @@ package org.matrix.android.sdk.internal.crypto.store.db
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
@@ -85,10 +89,6 @@ import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmException
|
||||
import timber.log.Timber
|
||||
@@ -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
|
||||
@@ -541,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
deviceId = it.deviceId
|
||||
)
|
||||
}
|
||||
monarchy.writeAsync { realm ->
|
||||
doRealmTransactionAsync(realmConfiguration) { realm ->
|
||||
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
|
||||
entities.forEach {
|
||||
realm.insertOrUpdate(it)
|
||||
@@ -1191,7 +1195,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
.findAll()
|
||||
.mapNotNull { entity ->
|
||||
when (entity.type) {
|
||||
GossipRequestType.KEY -> {
|
||||
GossipRequestType.KEY -> {
|
||||
IncomingRoomKeyRequest(
|
||||
userId = entity.otherUserId,
|
||||
deviceId = entity.otherDeviceId,
|
||||
|
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
@@ -398,7 +398,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
|
||||
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
|
||||
?.transform { deviceInfoEntity ->
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.store.db.model
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.GossipRequestType
|
||||
import org.matrix.android.sdk.internal.crypto.GossipingRequestState
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
|
||||
@@ -45,7 +45,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
|
||||
|
||||
var type: GossipRequestType
|
||||
get() {
|
||||
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||
return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||
}
|
||||
set(value) {
|
||||
typeStr = value.name
|
||||
@@ -55,7 +55,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
|
||||
|
||||
var requestState: GossipingRequestState
|
||||
get() {
|
||||
return tryThis { GossipingRequestState.valueOf(requestStateStr) }
|
||||
return tryOrNull { GossipingRequestState.valueOf(requestStateStr) }
|
||||
?: GossipingRequestState.NONE
|
||||
}
|
||||
set(value) {
|
||||
|
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Types
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.GossipRequestType
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
|
||||
@@ -47,7 +47,7 @@ internal open class OutgoingGossipingRequestEntity(
|
||||
|
||||
var type: GossipRequestType
|
||||
get() {
|
||||
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||
return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
|
||||
}
|
||||
set(value) {
|
||||
typeStr = value.name
|
||||
@@ -57,7 +57,7 @@ internal open class OutgoingGossipingRequestEntity(
|
||||
|
||||
var requestState: OutgoingGossipingRequestState
|
||||
get() {
|
||||
return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) }
|
||||
return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) }
|
||||
?: OutgoingGossipingRequestState.UNSENT
|
||||
}
|
||||
set(value) {
|
||||
|
@@ -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 ->
|
||||
|
@@ -17,17 +17,17 @@
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -37,56 +37,56 @@ import javax.inject.Inject
|
||||
*/
|
||||
internal class SendVerificationMessageWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<SendVerificationMessageWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val event: Event,
|
||||
val eventId: String,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject
|
||||
lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var cryptoService: CryptoService
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
|
||||
@Inject
|
||||
lateinit var cryptoService: CryptoService
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found")
|
||||
val localEventId = localEvent.eventId ?: ""
|
||||
val roomId = localEvent.roomId ?: ""
|
||||
|
||||
if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) {
|
||||
return Result.success()
|
||||
.also {
|
||||
cancelSendTracker.markCancelled(localEventId, roomId)
|
||||
Timber.e("## SendEvent: Event sending has been cancelled $localEventId")
|
||||
}
|
||||
}
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
val localId = params.event.eventId ?: ""
|
||||
return try {
|
||||
val eventId = sendVerificationMessageTask.execute(
|
||||
val resultEventId = sendVerificationMessageTask.execute(
|
||||
SendVerificationMessageTask.Params(
|
||||
event = params.event,
|
||||
event = localEvent,
|
||||
cryptoService = cryptoService
|
||||
)
|
||||
)
|
||||
|
||||
Result.success(Data.Builder().putString(localId, eventId).build())
|
||||
} catch (exception: Throwable) {
|
||||
if (exception.shouldBeRetried()) {
|
||||
Result.success(Data.Builder().putString(localEventId, resultEventId).build())
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.success(errorOutputData)
|
||||
buildErrorResult(params, throwable.localizedMessage ?: "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val OUTPUT_KEY_FAILED = "failed"
|
||||
|
||||
fun hasFailed(outputData: Data): Boolean {
|
||||
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
|
||||
}
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,9 @@ import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.Operation
|
||||
import androidx.work.WorkInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.R
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
||||
@@ -51,10 +54,8 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.StringProvider
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -87,7 +88,7 @@ internal class VerificationTransportRoomMessage(
|
||||
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
event = event
|
||||
eventId = event.eventId ?: ""
|
||||
))
|
||||
val enqueueInfo = enqueueSendWork(workerParams)
|
||||
|
||||
@@ -115,20 +116,30 @@ internal class VerificationTransportRoomMessage(
|
||||
val observer = object : Observer<List<WorkInfo>> {
|
||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||
workInfoList
|
||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||
?.firstOrNull { it.id == enqueueInfo.second }
|
||||
?.let { wInfo ->
|
||||
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
|
||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
||||
tx?.cancel(onErrorReason)
|
||||
} else {
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx?.state = nextState
|
||||
when (wInfo.state) {
|
||||
WorkInfo.State.FAILED -> {
|
||||
tx?.cancel(onErrorReason)
|
||||
workLiveData.removeObserver(this)
|
||||
}
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
|
||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
||||
tx?.cancel(onErrorReason)
|
||||
} else {
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx?.state = nextState
|
||||
}
|
||||
}
|
||||
workLiveData.removeObserver(this)
|
||||
}
|
||||
else -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
workLiveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +185,7 @@ internal class VerificationTransportRoomMessage(
|
||||
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
event = event
|
||||
eventId = event.eventId ?: ""
|
||||
))
|
||||
|
||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||
@@ -184,7 +195,7 @@ internal class VerificationTransportRoomMessage(
|
||||
.build()
|
||||
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||
.enqueue()
|
||||
|
||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||
@@ -199,7 +210,7 @@ internal class VerificationTransportRoomMessage(
|
||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||
?.firstOrNull { it.id == workRequest.id }
|
||||
?.let { wInfo ->
|
||||
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
|
||||
if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
|
||||
callback(null, null)
|
||||
} else {
|
||||
val eventId = wInfo.outputData.getString(localId)
|
||||
@@ -229,7 +240,7 @@ internal class VerificationTransportRoomMessage(
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
event = event
|
||||
eventId = event.eventId ?: ""
|
||||
))
|
||||
enqueueSendWork(workerParams)
|
||||
}
|
||||
@@ -249,7 +260,7 @@ internal class VerificationTransportRoomMessage(
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
event = event
|
||||
eventId = event.eventId ?: ""
|
||||
))
|
||||
val enqueueInfo = enqueueSendWork(workerParams)
|
||||
|
||||
@@ -280,7 +291,7 @@ internal class VerificationTransportRoomMessage(
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return workManagerProvider.workManager
|
||||
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest)
|
||||
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||
.enqueue() to workRequest.id
|
||||
}
|
||||
|
||||
|
@@ -16,31 +16,52 @@
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) {
|
||||
Realm.getInstance(config).use { bgRealm ->
|
||||
bgRealm.beginTransaction()
|
||||
val result: T
|
||||
try {
|
||||
val start = System.currentTimeMillis()
|
||||
result = transaction(bgRealm)
|
||||
if (isActive) {
|
||||
bgRealm.commitTransaction()
|
||||
val end = System.currentTimeMillis()
|
||||
val time = end - start
|
||||
Timber.v("Execute transaction in $time millis")
|
||||
}
|
||||
} finally {
|
||||
if (bgRealm.isInTransaction) {
|
||||
bgRealm.cancelTransaction()
|
||||
}
|
||||
}
|
||||
result
|
||||
internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) {
|
||||
asyncTransaction(monarchy.realmConfiguration, transaction)
|
||||
}
|
||||
|
||||
internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) {
|
||||
launch {
|
||||
awaitTransaction(realmConfiguration, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
private val realmSemaphore = Semaphore(1)
|
||||
|
||||
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
|
||||
return realmSemaphore.withPermit {
|
||||
withContext(Dispatchers.IO) {
|
||||
Realm.getInstance(config).use { bgRealm ->
|
||||
bgRealm.beginTransaction()
|
||||
val result: T
|
||||
try {
|
||||
val start = System.currentTimeMillis()
|
||||
result = transaction(bgRealm)
|
||||
if (isActive) {
|
||||
bgRealm.commitTransaction()
|
||||
val end = System.currentTimeMillis()
|
||||
val time = end - start
|
||||
Timber.v("Execute transaction in $time millis")
|
||||
}
|
||||
} finally {
|
||||
if (bgRealm.isInTransaction) {
|
||||
bgRealm.cancelTransaction()
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,21 +20,34 @@ package org.matrix.android.sdk.internal.database.mapper
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
|
||||
|
||||
internal object ContentMapper {
|
||||
|
||||
private val moshi = MoshiProvider.providesMoshi()
|
||||
private val adapter = moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
private val castJsonNumberMoshi by lazy {
|
||||
// We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row
|
||||
// and we lost typing information doing so.
|
||||
// We don't want this check to be done on all adapters, so we create a new moshi just for that.
|
||||
MoshiProvider.providesMoshi()
|
||||
.newBuilder()
|
||||
.add(CheckNumberType.JSON_ADAPTER_FACTORY)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun map(content: String?): Content? {
|
||||
fun map(content: String?, castJsonNumbers: Boolean = false): Content? {
|
||||
return content?.let {
|
||||
adapter.fromJson(it)
|
||||
if (castJsonNumbers) {
|
||||
castJsonNumberMoshi
|
||||
} else {
|
||||
moshi
|
||||
}.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).fromJson(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun map(content: Content?): String? {
|
||||
return content?.let {
|
||||
adapter.toJson(it)
|
||||
moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).toJson(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -54,7 +54,7 @@ internal object EventMapper {
|
||||
return eventEntity
|
||||
}
|
||||
|
||||
fun map(eventEntity: EventEntity): Event {
|
||||
fun map(eventEntity: EventEntity, castJsonNumbers: Boolean = false): Event {
|
||||
val ud = eventEntity.unsignedData
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
@@ -69,8 +69,8 @@ internal object EventMapper {
|
||||
return Event(
|
||||
type = eventEntity.type,
|
||||
eventId = eventEntity.eventId,
|
||||
content = ContentMapper.map(eventEntity.content),
|
||||
prevContent = ContentMapper.map(eventEntity.prevContent),
|
||||
content = ContentMapper.map(eventEntity.content, castJsonNumbers),
|
||||
prevContent = ContentMapper.map(eventEntity.prevContent, castJsonNumbers),
|
||||
originServerTs = eventEntity.originServerTs,
|
||||
senderId = eventEntity.sender,
|
||||
stateKey = eventEntity.stateKey,
|
||||
@@ -96,8 +96,8 @@ internal object EventMapper {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EventEntity.asDomain(): Event {
|
||||
return EventMapper.map(this)
|
||||
internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
|
||||
return EventMapper.map(this, castJsonNumbers)
|
||||
}
|
||||
|
||||
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -24,7 +24,7 @@ import okio.BufferedSink
|
||||
import okio.ForwardingSink
|
||||
import okio.Sink
|
||||
import okio.buffer
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import java.io.IOException
|
||||
|
||||
internal class ProgressRequestBody(private val delegate: RequestBody,
|
||||
@@ -40,7 +40,7 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
|
||||
|
||||
override fun isDuplex() = delegate.isDuplex()
|
||||
|
||||
val length = tryThis { delegate.contentLength() } ?: -1
|
||||
val length = tryOrNull { delegate.contentLength() } ?: -1
|
||||
|
||||
override fun contentLength() = length
|
||||
|
||||
|
@@ -49,7 +49,7 @@ interface CheckNumberType {
|
||||
val numberAsString = reader.nextString()
|
||||
val decimal = BigDecimal(numberAsString)
|
||||
if (decimal.scale() <= 0) {
|
||||
decimal.intValueExact()
|
||||
decimal.longValueExact()
|
||||
} else {
|
||||
decimal.toDouble()
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.FileProvider
|
||||
import arrow.core.Try
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
@@ -174,7 +174,7 @@ internal class DefaultFileService @Inject constructor(
|
||||
}
|
||||
}
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryThis { otherCallbacks.onFailure(it) }
|
||||
tryOrNull { otherCallbacks.onFailure(it) }
|
||||
}
|
||||
}, { file ->
|
||||
callback.onSuccess(file)
|
||||
@@ -186,7 +186,7 @@ internal class DefaultFileService @Inject constructor(
|
||||
}
|
||||
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryThis { otherCallbacks.onSuccess(file) }
|
||||
tryOrNull { otherCallbacks.onSuccess(file) }
|
||||
}
|
||||
})
|
||||
}.toCancelable()
|
||||
|
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.call
|
||||
|
||||
import android.os.SystemClock
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.CallsListener
|
||||
@@ -210,7 +210,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||
|
||||
private fun onCallHangup(hangup: CallHangupContent) {
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
it.onCallHangupReceived(hangup)
|
||||
}
|
||||
}
|
||||
@@ -218,7 +218,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||
|
||||
private fun onCallAnswer(answer: CallAnswerContent) {
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
it.onCallAnswerReceived(answer)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||
|
||||
private fun onCallManageByOtherSession(callId: String) {
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
it.onCallManagedByOtherSession(callId)
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||
if (incomingCall.otherUserId == userId) return
|
||||
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
it.onCallInviteReceived(incomingCall, invite)
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||
|
||||
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
|
||||
callListeners.toList().forEach {
|
||||
tryThis {
|
||||
tryOrNull {
|
||||
it.onCallIceCandidateReceived(incomingCall, candidates)
|
||||
}
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okio.BufferedSink
|
||||
import okio.source
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.internal.di.Authenticated
|
||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||
@@ -96,7 +96,7 @@ internal class FileUploader @Inject constructor(@Authenticated
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
return uploadFile(workingFile, filename, mimeType, progressListener).also {
|
||||
tryThis { workingFile.delete() }
|
||||
tryOrNull { workingFile.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,12 +19,10 @@ package org.matrix.android.sdk.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
@@ -34,13 +32,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||
import org.matrix.android.sdk.internal.session.DefaultFileService
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
@@ -56,12 +59,13 @@ private data class NewImageAttributes(
|
||||
* Possible previous worker: None
|
||||
* Possible next worker : Always [MultipleEventSendingDispatcherWorker]
|
||||
*/
|
||||
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
internal class UploadContentWorker(val context: Context, params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val events: List<Event>,
|
||||
val localEchoIds: List<LocalEchoIdentifiers>,
|
||||
val attachment: ContentAttachmentData,
|
||||
val isEncrypted: Boolean,
|
||||
val compressBeforeSending: Boolean,
|
||||
@@ -73,20 +77,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
@Inject lateinit var fileService: DefaultFileService
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
@Inject lateinit var imageCompressor: ImageCompressor
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.v("Starting upload media work with params $params")
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
}
|
||||
|
||||
// Just defensive code to ensure that we never have an uncaught exception that could break the queue
|
||||
return try {
|
||||
internalDoWork(params)
|
||||
@@ -96,11 +94,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalDoWork(params: Params): Result {
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
|
||||
private suspend fun internalDoWork(params: Params): Result {
|
||||
val allCancelled = params.localEchoIds.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
|
||||
if (allCancelled) {
|
||||
// there is no point in uploading the image!
|
||||
return Result.success(inputData)
|
||||
@@ -214,18 +213,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## FileService: ERROR")
|
||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||
return Result.success(
|
||||
WorkerParamsFactory.toData(
|
||||
params.copy(
|
||||
lastFailureMessage = e.localizedMessage
|
||||
)
|
||||
)
|
||||
)
|
||||
return handleFailure(params, e)
|
||||
} finally {
|
||||
// Delete all temporary files
|
||||
filesToDelete.forEach {
|
||||
tryThis { it.delete() }
|
||||
tryOrNull { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,46 +281,48 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleSuccess(params: Params,
|
||||
attachmentUrl: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String?,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Result {
|
||||
private suspend fun handleSuccess(params: Params,
|
||||
attachmentUrl: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String?,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Result {
|
||||
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
|
||||
params.localEchoIds.forEach {
|
||||
updateEvent(it.eventId, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
|
||||
}
|
||||
|
||||
val updatedEvents = params.events
|
||||
.map {
|
||||
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
|
||||
}
|
||||
|
||||
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
|
||||
val sendParams = MultipleEventSendingDispatcherWorker.Params(
|
||||
sessionId = params.sessionId,
|
||||
localEchoIds = params.localEchoIds,
|
||||
isEncrypted = params.isEncrypted
|
||||
)
|
||||
return Result.success(WorkerParamsFactory.toData(sendParams)).also {
|
||||
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEvent(event: Event,
|
||||
url: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String? = null,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Event {
|
||||
val messageContent: MessageContent = event.content.toModel() ?: return event
|
||||
val updatedContent = when (messageContent) {
|
||||
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes)
|
||||
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||
is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
|
||||
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
|
||||
else -> messageContent
|
||||
private suspend fun updateEvent(eventId: String,
|
||||
url: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String? = null,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?) {
|
||||
localEchoRepository.updateEcho(eventId) { _, event ->
|
||||
val messageContent: MessageContent? = event.asDomain().content.toModel()
|
||||
val updatedContent = when (messageContent) {
|
||||
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes)
|
||||
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||
is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
|
||||
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
|
||||
else -> messageContent
|
||||
}
|
||||
event.content = ContentMapper.map(updatedContent.toContent())
|
||||
}
|
||||
return event.copy(content = updatedContent.toContent())
|
||||
}
|
||||
|
||||
private fun notifyTracker(params: Params, function: (String) -> Unit) {
|
||||
params.events
|
||||
.mapNotNull { it.eventId }
|
||||
.forEach { eventId -> function.invoke(eventId) }
|
||||
params.localEchoIds.forEach { function.invoke(it.eventId) }
|
||||
}
|
||||
|
||||
private fun MessageImageContent.update(url: String,
|
||||
|
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.download
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
@@ -76,7 +76,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
|
||||
Timber.v("## DL Progress Error code:$errorCode")
|
||||
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
|
||||
listeners[url]?.forEach {
|
||||
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
|
||||
tryOrNull { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
|
||||
private fun updateState(url: String, state: ContentDownloadStateTracker.State) {
|
||||
states[url] = state
|
||||
listeners[url]?.forEach {
|
||||
tryThis { it.onDownloadStateUpdate(state) }
|
||||
tryOrNull { it.onDownloadStateUpdate(state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,20 +18,19 @@
|
||||
package org.matrix.android.sdk.internal.session.group
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Possible previous worker: None
|
||||
* Possible next worker : None
|
||||
*/
|
||||
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
internal class GetGroupDataWorker(context: Context, params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<GetGroupDataWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
@@ -41,13 +40,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
||||
|
||||
@Inject lateinit var getGroupDataTask: GetGroupDataTask
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
return runCatching {
|
||||
getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
|
||||
}.fold(
|
||||
@@ -55,4 +52,8 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
||||
{ Result.retry() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleRegistry
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@@ -113,7 +113,7 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
// Url has changed, we have to reset our store, update internal configuration and notify listeners
|
||||
identityStore.setUrl(baseUrl)
|
||||
updateIdentityAPI(baseUrl)
|
||||
listeners.toList().forEach { tryThis { it.onIdentityServerChange() } }
|
||||
listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ internal class DefaultIdentityService @Inject constructor(
|
||||
private suspend fun updateAccountData(url: String?) {
|
||||
// Also notify the listener
|
||||
withContext(coroutineDispatchers.main) {
|
||||
listeners.toList().forEach { tryThis { it.onIdentityServerChange() } }
|
||||
listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
|
||||
}
|
||||
|
||||
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams(
|
||||
|
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -39,7 +40,7 @@ internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val p
|
||||
|
||||
Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids")
|
||||
// Store the list in DB
|
||||
monarchy.writeAsync { realm ->
|
||||
monarchy.awaitTransaction { realm ->
|
||||
realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm()
|
||||
accountThreePidsResponse.threePids?.forEach {
|
||||
val entity = UserThreePidEntity(
|
||||
|
@@ -17,10 +17,10 @@
|
||||
package org.matrix.android.sdk.internal.session.pushers
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.pushers.PusherState
|
||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||
@@ -28,16 +28,14 @@ import org.matrix.android.sdk.internal.database.model.PusherEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<AddHttpPusherWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
@@ -50,14 +48,11 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||
@Inject @SessionDatabase lateinit var monarchy: Monarchy
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val pusher = params.pusher
|
||||
|
||||
if (pusher.pushKey.isBlank()) {
|
||||
@@ -82,6 +77,10 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private suspend fun setPusher(pusher: JsonPusher) {
|
||||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = pushersAPI.setPusher(pusher)
|
||||
|
@@ -202,13 +202,13 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||
|
||||
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event, keepKeys)
|
||||
val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.session.room.relation
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -27,45 +26,38 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
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.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO This is not used. Delete?
|
||||
internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
internal class SendRelationWorker(context: Context, params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<SendRelationWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val roomId: String,
|
||||
val event: Event,
|
||||
val eventId: String,
|
||||
val relationType: String? = null,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var roomAPI: RoomAPI
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
}
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localEvent = params.event
|
||||
if (localEvent.eventId == null) {
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
|
||||
if (localEvent?.eventId == null) {
|
||||
return Result.failure()
|
||||
}
|
||||
val relationContent = localEvent.content.toModel<ReactionContent>()
|
||||
@@ -88,6 +80,10 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) {
|
||||
executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.sendRelation(
|
||||
|
@@ -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)
|
||||
@@ -336,7 +351,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
return EncryptEventWorker.Params(sessionId, event)
|
||||
return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
|
||||
.let { WorkerParamsFactory.toData(it) }
|
||||
.let {
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
@@ -360,7 +375,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
attachment: ContentAttachmentData,
|
||||
isRoomEncrypted: Boolean,
|
||||
compressBeforeSending: Boolean): OneTimeWorkRequest {
|
||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending)
|
||||
val localEchoIds = allLocalEchos.map {
|
||||
LocalEchoIdentifiers(it.roomId!!, it.eventId!!)
|
||||
}
|
||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, localEchoIds, attachment, isRoomEncrypted, compressBeforeSending)
|
||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||
|
@@ -18,21 +18,24 @@
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
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
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -41,12 +44,12 @@ import javax.inject.Inject
|
||||
* Possible next worker : Always [SendEventWorker]
|
||||
*/
|
||||
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<EncryptEventWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val event: Event,
|
||||
val eventId: String,
|
||||
/** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
|
||||
val keepKeys: List<String>? = null,
|
||||
override val lastFailureMessage: String? = null
|
||||
@@ -56,24 +59,19 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.v("Start Encrypt work")
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}")
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
}
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
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)
|
||||
|
||||
val localEvent = params.event
|
||||
if (localEvent.eventId == null) {
|
||||
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.ENCRYPT_WORKER)
|
||||
if (localEvent?.eventId == null) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@@ -93,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
|
||||
@@ -106,15 +104,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
modifiedContent[toKeep] = it
|
||||
}
|
||||
}
|
||||
val safeResult = result.copy(eventContent = modifiedContent)
|
||||
val encryptedEvent = localEvent.copy(
|
||||
type = safeResult.eventType,
|
||||
content = safeResult.eventContent
|
||||
)
|
||||
// Better handling of local echo, to avoid decrypting transition on remote echo
|
||||
// Should I only do it for text messages?
|
||||
if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
val decryptionLocalEcho = MXEventDecryptionResult(
|
||||
val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = Event(
|
||||
type = localEvent.type,
|
||||
content = localEvent.content,
|
||||
@@ -124,10 +117,18 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = crypto.getMyDevice().fingerprint()
|
||||
)
|
||||
localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent)
|
||||
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
|
||||
localEcho.type = EventType.ENCRYPTED
|
||||
localEcho.content = ContentMapper.map(modifiedContent)
|
||||
decryptionLocalEcho?.also {
|
||||
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 {
|
||||
val sendState = when (error) {
|
||||
@@ -138,10 +139,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
// always return success, or the chain will be stuck for ever!
|
||||
val nextWorkerParams = SendEventWorker.Params(
|
||||
sessionId = params.sessionId,
|
||||
event = localEvent,
|
||||
eventId = localEvent.eventId,
|
||||
lastFailureMessage = error?.localizedMessage ?: "Error"
|
||||
)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.internal.session.room.send
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This is used as a holder to pass necessary data to some workers params.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class LocalEchoIdentifiers(val roomId: String, val eventId: String)
|
@@ -18,20 +18,21 @@
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
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.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.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.asyncTransaction
|
||||
import org.matrix.android.sdk.internal.database.helper.nextId
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||
@@ -44,11 +45,13 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val realmSessionProvider: RealmSessionProvider,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val eventBus: EventBus,
|
||||
@@ -60,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)
|
||||
@@ -76,43 +80,55 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
||||
}
|
||||
val timelineEvent = timelineEventMapper.map(timelineEventEntity)
|
||||
eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent))
|
||||
monarchy.writeAsync { realm ->
|
||||
taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
|
||||
val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply {
|
||||
this.insertType = EventInsertType.LOCAL_ECHO
|
||||
}
|
||||
realm.insert(eventInsertEntity)
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSendState(eventId: String, sendState: SendState) {
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
|
||||
monarchy.writeAsync { realm ->
|
||||
updateEchoAsync(eventId) { realm, sendingEventEntity ->
|
||||
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
|
||||
// If already synced, do not put as sent
|
||||
} else {
|
||||
sendingEventEntity.sendState = sendState
|
||||
}
|
||||
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateEcho(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
|
||||
if (sendingEventEntity != null) {
|
||||
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
|
||||
// If already synced, do not put as sent
|
||||
} else {
|
||||
sendingEventEntity.sendState = sendState
|
||||
}
|
||||
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
|
||||
block(realm, sendingEventEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) {
|
||||
monarchy.writeAsync { realm ->
|
||||
fun updateEchoAsync(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) {
|
||||
taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
|
||||
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
|
||||
if (sendingEventEntity != null) {
|
||||
sendingEventEntity.type = EventType.ENCRYPTED
|
||||
sendingEventEntity.content = ContentMapper.map(encryptedContent)
|
||||
sendingEventEntity.setDecryptionResult(mxEventDecryptionResult)
|
||||
block(realm, sendingEventEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUpToDateEcho(eventId: String): Event? {
|
||||
// We are using awaitTransaction here to make sure this executes after other transactions
|
||||
return monarchy.awaitTransaction { realm ->
|
||||
EventEntity.where(realm, eventId).findFirst()?.asDomain(castJsonNumbers = true)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
|
||||
deleteFailedEcho(roomId, localEcho.eventId)
|
||||
}
|
||||
@@ -150,7 +166,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
||||
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
|
||||
}
|
||||
|
||||
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> {
|
||||
fun getAllEventsWithStates(roomId: String, states: List<SendState>): List<TimelineEvent> {
|
||||
return realmSessionProvider.withRealm { realm ->
|
||||
TimelineEventEntity
|
||||
.findAllInRoomWithSendStates(realm, roomId, states)
|
||||
|
@@ -19,18 +19,17 @@ package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -43,12 +42,12 @@ import javax.inject.Inject
|
||||
* Possible next worker : None, but it will post new work to send events, encrypted or not
|
||||
*/
|
||||
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<MultipleEventSendingDispatcherWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val events: List<Event>,
|
||||
val localEchoIds: List<LocalEchoIdentifiers>,
|
||||
val isEncrypted: Boolean,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
@@ -57,46 +56,48 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
||||
@Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.v("## SendEvent: Start dispatch sending multiple event work")
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
params.events.forEach { event ->
|
||||
event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) }
|
||||
}
|
||||
// Transmit the error if needed?
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") }
|
||||
override fun doOnError(params: Params): Result {
|
||||
params.localEchoIds.forEach { localEchoIds ->
|
||||
localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED)
|
||||
}
|
||||
|
||||
return super.doOnError(params)
|
||||
}
|
||||
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.v("## SendEvent: Start dispatch sending multiple event work")
|
||||
// Create a work for every event
|
||||
params.events.forEach { event ->
|
||||
params.localEchoIds.forEach { localEchoIds ->
|
||||
val roomId = localEchoIds.roomId
|
||||
val eventId = localEchoIds.eventId
|
||||
if (params.isEncrypted) {
|
||||
localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
|
||||
val encryptWork = createEncryptEventWork(params.sessionId, event, true)
|
||||
localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
|
||||
val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(params.sessionId, event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork)
|
||||
val sendWork = createSendEventWork(params.sessionId, eventId, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||
} else {
|
||||
localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
|
||||
val sendWork = createSendEventWork(params.sessionId, event, true)
|
||||
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork)
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENDING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
|
||||
val sendWork = createSendEventWork(params.sessionId, eventId, true)
|
||||
timelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val params = EncryptEventWorker.Params(sessionId, event)
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
|
||||
val params = EncryptEventWorker.Params(sessionId, eventId)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
@@ -107,8 +108,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
|
||||
private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
|
@@ -17,24 +17,24 @@
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Possible previous worker: None
|
||||
* Possible next worker : None
|
||||
*/
|
||||
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<RedactEventWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
@@ -49,20 +49,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
||||
@Inject lateinit var roomAPI: RoomAPI
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
}
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val eventId = params.eventId
|
||||
return runCatching {
|
||||
executeRequest<SendResponse>(eventBus) {
|
||||
@@ -91,4 +82,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
||||
|
@@ -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 ?: "")
|
||||
@@ -56,7 +57,7 @@ internal class RoomEventSender @Inject constructor(
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event)
|
||||
val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
@@ -68,7 +69,7 @@ internal class RoomEventSender @Inject constructor(
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
|
@@ -18,19 +18,20 @@
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
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.events.model.Event
|
||||
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
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -42,35 +43,30 @@ import javax.inject.Inject
|
||||
*/
|
||||
internal class SendEventWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
: SessionSafeCoroutineWorker<SendEventWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
override val lastFailureMessage: String? = null,
|
||||
val event: Event? = null,
|
||||
// Keep for compat at the moment, will be removed later
|
||||
val eventId: String? = null
|
||||
val eventId: String
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var roomAPI: RoomAPI
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success()
|
||||
.also { Timber.e("## SendEvent: Unable to parse work parameters") }
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
val event = params.event
|
||||
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) {
|
||||
// Old way of sending
|
||||
if (params.eventId != null) {
|
||||
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
|
||||
}
|
||||
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
|
||||
return Result.success()
|
||||
.also { Timber.e("Work cancelled due to bad input data") }
|
||||
}
|
||||
@@ -93,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,11 +103,17 @@ internal class SendEventWorker(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@@ -333,12 +334,22 @@ internal class DefaultTimeline(
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
// Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = builder(te)
|
||||
true
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
|
||||
return tryOrNull {
|
||||
builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
// Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
val rebuiltEvent = builder(te)
|
||||
// If rebuilt event is filtered its returned as null and should be removed.
|
||||
if (rebuiltEvent == null) {
|
||||
builtEventsIdMap.remove(eventId)
|
||||
builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) }
|
||||
builtEvents.removeAt(builtIndex)
|
||||
} else {
|
||||
builtEvents[builtIndex] = rebuiltEvent
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
@@ -413,14 +424,14 @@ internal class DefaultTimeline(
|
||||
|
||||
private fun getState(direction: Timeline.Direction): State {
|
||||
return when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.BACKWARDS -> backwardsState.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
|
||||
val stateReference = when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.BACKWARDS -> backwardsState
|
||||
}
|
||||
val currentValue = stateReference.get()
|
||||
@@ -489,7 +500,8 @@ internal class DefaultTimeline(
|
||||
val eventEntity = results[index]
|
||||
eventEntity?.eventId?.let { eventId ->
|
||||
postSnapshot = rebuildEvent(eventId) {
|
||||
buildTimelineEvent(eventEntity)
|
||||
val builtEvent = buildTimelineEvent(eventEntity)
|
||||
listOf(builtEvent).filterEventsWithSettings().firstOrNull()
|
||||
} || postSnapshot
|
||||
}
|
||||
}
|
||||
@@ -730,10 +742,10 @@ internal class DefaultTimeline(
|
||||
return object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
when (data) {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||
}
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
postSnapshot()
|
||||
}
|
||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||
@@ -775,9 +787,8 @@ internal class DefaultTimeline(
|
||||
}
|
||||
if (!filterEdits) return@filter false
|
||||
|
||||
val filterRedacted = !settings.filters.filterRedacted || it.root.isRedacted()
|
||||
|
||||
filterRedacted
|
||||
val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted()
|
||||
!filterRedacted
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -131,7 +131,7 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
|
||||
|
||||
/**
|
||||
* At the moment we don't get any group data through the sync, so we poll where every hour.
|
||||
You can also force to refetch group data using [Group] API.
|
||||
* You can also force to refetch group data using [Group] API.
|
||||
*/
|
||||
private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) {
|
||||
val groupIds = ArrayList<String>()
|
||||
|
@@ -18,18 +18,18 @@ package org.matrix.android.sdk.internal.session.sync.job
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.failure.isTokenError
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
@@ -43,7 +43,7 @@ private const val DEFAULT_DELAY_TIMEOUT = 30_000L
|
||||
*/
|
||||
internal class SyncWorker(context: Context,
|
||||
workerParameters: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
) : SessionSafeCoroutineWorker<SyncWorker.Params>(context, workerParameters, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
@@ -59,14 +59,13 @@ internal class SyncWorker(context: Context,
|
||||
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
|
||||
@Inject lateinit var workManagerProvider: WorkManagerProvider
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.i("Sync work starting")
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.i("Sync work starting")
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
return runCatching {
|
||||
doSync(params.timeout)
|
||||
}.fold(
|
||||
@@ -91,6 +90,10 @@ internal class SyncWorker(context: Context,
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private suspend fun doSync(timeout: Long) {
|
||||
val taskParams = SyncTask.Params(timeout * 1000)
|
||||
syncTask.execute(taskParams)
|
||||
|
@@ -202,6 +202,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
|
||||
stateKey = QueryStringValue.NoCondition
|
||||
)
|
||||
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
|
||||
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null)
|
||||
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
|
||||
}
|
||||
}
|
||||
|
@@ -18,9 +18,9 @@
|
||||
package org.matrix.android.sdk.internal.util
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.internal.database.awaitTransaction
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmModel
|
||||
import org.matrix.android.sdk.internal.database.awaitTransaction
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T {
|
||||
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.internal.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This worker should only sends Result.Success when added to a unique queue to avoid breaking the unique queue.
|
||||
* This abstract class handle the cases of problem when parsing parameter, and forward the error if any to
|
||||
* the next workers.
|
||||
*/
|
||||
internal abstract class SessionSafeCoroutineWorker<PARAM : SessionWorkerParams>(
|
||||
context: Context,
|
||||
workerParameters: WorkerParameters,
|
||||
private val paramClass: Class<PARAM>
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class ErrorData(
|
||||
override val sessionId: String,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
final override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData(paramClass, inputData)
|
||||
?: return buildErrorResult(null, "Unable to parse work parameters")
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
return try {
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return buildErrorResult(params, "No session")
|
||||
|
||||
// Make sure to inject before handling error as you may need some dependencies to process them.
|
||||
injectWith(sessionComponent)
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Forward error to the next workers
|
||||
doOnError(params)
|
||||
} else {
|
||||
doSafeWork(params)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
buildErrorResult(params, throwable.localizedMessage ?: "error")
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun injectWith(injector: SessionComponent)
|
||||
|
||||
/**
|
||||
* Should only return Result.Success for workers added to a unique queue
|
||||
*/
|
||||
abstract suspend fun doSafeWork(params: PARAM): Result
|
||||
|
||||
protected fun buildErrorResult(params: PARAM?, message: String): Result {
|
||||
return Result.success(
|
||||
if (params != null) {
|
||||
WorkerParamsFactory.toData(paramClass, buildErrorParams(params, message))
|
||||
} else {
|
||||
WorkerParamsFactory.toData(ErrorData::class.java, ErrorData(sessionId = "", lastFailureMessage = message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
abstract fun buildErrorParams(params: PARAM, message: String): PARAM
|
||||
|
||||
/**
|
||||
* This is called when the input parameters are correct, but contain an error from the previous worker.
|
||||
*/
|
||||
@CallSuper
|
||||
open fun doOnError(params: PARAM): Result {
|
||||
// Forward the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun hasFailed(outputData: Data): Boolean {
|
||||
return WorkerParamsFactory.fromData(ErrorData::class.java, outputData)
|
||||
.let { it?.lastFailureMessage != null }
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,12 +18,14 @@
|
||||
package org.matrix.android.sdk.internal.worker
|
||||
|
||||
import androidx.work.Data
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
|
||||
|
||||
internal object WorkerParamsFactory {
|
||||
|
||||
val moshi by lazy {
|
||||
private val moshi: Moshi by lazy {
|
||||
// We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row
|
||||
// and we lost typing information doing so.
|
||||
// We don't want this check to be done on all adapters, so we just add it here.
|
||||
@@ -33,20 +35,24 @@ internal object WorkerParamsFactory {
|
||||
.build()
|
||||
}
|
||||
|
||||
const val KEY = "WORKER_PARAMS_JSON"
|
||||
private const val KEY = "WORKER_PARAMS_JSON"
|
||||
|
||||
inline fun <reified T> toData(params: T): Data {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
inline fun <reified T> toData(params: T) = toData(T::class.java, params)
|
||||
|
||||
fun <T> toData(clazz: Class<T>, params: T): Data {
|
||||
val adapter = moshi.adapter(clazz)
|
||||
val json = adapter.toJson(params)
|
||||
return Data.Builder().putString(KEY, json).build()
|
||||
}
|
||||
|
||||
inline fun <reified T> fromData(data: Data): T? {
|
||||
inline fun <reified T> fromData(data: Data) = fromData(T::class.java, data)
|
||||
|
||||
fun <T> fromData(clazz: Class<T>, data: Data): T? = tryOrNull("Unable to parse work parameters") {
|
||||
val json = data.getString(KEY)
|
||||
return if (json == null) {
|
||||
null
|
||||
} else {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
val adapter = moshi.adapter(clazz)
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
|
@@ -151,21 +151,21 @@
|
||||
<string name="notice_power_level_changed">%1$s alterou o nível de permissão de %2$s.</string>
|
||||
<string name="notice_power_level_diff">%1$s de %2$s para %3$s</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account">Primeira sincronização:
|
||||
\nImportando a conta…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_crypto">Primeira sincronização:
|
||||
\nImportando as chaves de criptografia</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_rooms">Primeira sincronização:
|
||||
\nImportando as salas</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Primeira sincronização:
|
||||
\nImportando as salas em que você entrou</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Primeira sincronização:
|
||||
\nImportando as salas em que você foi convidado</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Primeira sincronização:
|
||||
\nImportando as salas em que você saiu</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_groups">Primeira sincronização:
|
||||
\nImportando as comunidades</string>
|
||||
<string name="initial_sync_start_importing_account_data">Primeira sincronização:↵
|
||||
<string name="initial_sync_start_importing_account_data">Primeira sincronização:
|
||||
\nImportando os dados da conta</string>
|
||||
|
||||
<string name="event_status_sending_message">Enviando mensagem…</string>
|
||||
|
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Generated file, do not edit -->
|
||||
<string name="verification_emoji_dog">Cachorro</string>
|
||||
<string name="verification_emoji_cat">Gato</string>
|
||||
<string name="verification_emoji_lion">Leão</string>
|
||||
<string name="verification_emoji_horse">Cavalo</string>
|
||||
<string name="verification_emoji_unicorn">Unicórnio</string>
|
||||
<string name="verification_emoji_pig">Porco</string>
|
||||
<string name="verification_emoji_elephant">Elefante</string>
|
||||
<string name="verification_emoji_rabbit">Coelho</string>
|
||||
<string name="verification_emoji_panda">Panda</string>
|
||||
<string name="verification_emoji_rooster">Galo</string>
|
||||
<string name="verification_emoji_penguin">Pinguim</string>
|
||||
<string name="verification_emoji_turtle">Tartaruga</string>
|
||||
<string name="verification_emoji_fish">Peixe</string>
|
||||
<string name="verification_emoji_octopus">Polvo</string>
|
||||
<string name="verification_emoji_butterfly">Borboleta</string>
|
||||
<string name="verification_emoji_flower">Flor</string>
|
||||
<string name="verification_emoji_tree">Árvore</string>
|
||||
<string name="verification_emoji_cactus">Cacto</string>
|
||||
<string name="verification_emoji_mushroom">Cogumelo</string>
|
||||
<string name="verification_emoji_globe">Globo</string>
|
||||
<string name="verification_emoji_moon">Lua</string>
|
||||
<string name="verification_emoji_cloud">Nuvem</string>
|
||||
<string name="verification_emoji_fire">Fogo</string>
|
||||
<string name="verification_emoji_banana">Banana</string>
|
||||
<string name="verification_emoji_apple">Maçã</string>
|
||||
<string name="verification_emoji_strawberry">Morango</string>
|
||||
<string name="verification_emoji_corn">Milho</string>
|
||||
<string name="verification_emoji_pizza">Pizza</string>
|
||||
<string name="verification_emoji_cake">Bolo</string>
|
||||
<string name="verification_emoji_heart">Coração</string>
|
||||
<string name="verification_emoji_smiley">Sorriso</string>
|
||||
<string name="verification_emoji_robot">Robô</string>
|
||||
<string name="verification_emoji_hat">Chapéu</string>
|
||||
<string name="verification_emoji_glasses">Óculos</string>
|
||||
<string name="verification_emoji_spanner">Chave inglesa</string>
|
||||
<string name="verification_emoji_santa">Papai-noel</string>
|
||||
<string name="verification_emoji_thumbs_up">Joinha</string>
|
||||
<string name="verification_emoji_umbrella">Guarda-chuva</string>
|
||||
<string name="verification_emoji_hourglass">Ampulheta</string>
|
||||
<string name="verification_emoji_clock">Relógio</string>
|
||||
<string name="verification_emoji_gift">Presente</string>
|
||||
<string name="verification_emoji_light_bulb">Lâmpada</string>
|
||||
<string name="verification_emoji_book">Livro</string>
|
||||
<string name="verification_emoji_pencil">Lápis</string>
|
||||
<string name="verification_emoji_paperclip">Clipe de papel</string>
|
||||
<string name="verification_emoji_scissors">Tesoura</string>
|
||||
<string name="verification_emoji_lock">Cadeado</string>
|
||||
<string name="verification_emoji_key">Chave</string>
|
||||
<string name="verification_emoji_hammer">Martelo</string>
|
||||
<string name="verification_emoji_telephone">Telefone</string>
|
||||
<string name="verification_emoji_flag">Bandeira</string>
|
||||
<string name="verification_emoji_train">Trem</string>
|
||||
<string name="verification_emoji_bicycle">Bicicleta</string>
|
||||
<string name="verification_emoji_aeroplane">Avião</string>
|
||||
<string name="verification_emoji_rocket">Foguete</string>
|
||||
<string name="verification_emoji_trophy">Troféu</string>
|
||||
<string name="verification_emoji_ball">Bola</string>
|
||||
<string name="verification_emoji_guitar">Guitarra</string>
|
||||
<string name="verification_emoji_trumpet">Trombeta</string>
|
||||
<string name="verification_emoji_bell">Sino</string>
|
||||
<string name="verification_emoji_anchor">Âncora</string>
|
||||
<string name="verification_emoji_headphones">Fones de ouvido</string>
|
||||
<string name="verification_emoji_folder">Pasta</string>
|
||||
<string name="verification_emoji_pin">Alfinete</string>
|
||||
</resources>
|
@@ -27,8 +27,8 @@
|
||||
<string name="notice_room_ban_by_you">Du bannade %1$s</string>
|
||||
<string name="notice_room_withdraw">%1$s drog tillbaka inbjudan för %2$s</string>
|
||||
<string name="notice_room_withdraw_by_you">Du drog tillbaka inbjudan för %1$s</string>
|
||||
<string name="notice_avatar_url_changed">%1$s ändrade sin avatar</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Du ändrade din avatar</string>
|
||||
<string name="notice_avatar_url_changed">%1$s bytte sin avatar</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Du bytte din avatar</string>
|
||||
<string name="notice_display_name_set">%1$s satte sitt visningsnamn till %2$s</string>
|
||||
<string name="notice_display_name_set_by_you">Du satte ditt visningsnamn till %1$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s bytte sitt visningsnamn från %2$s till %3$s</string>
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<string name="verification_emoji_banana">Banan</string>
|
||||
<string name="verification_emoji_apple">Äpple</string>
|
||||
<string name="verification_emoji_strawberry">Jordgubbe</string>
|
||||
<string name="verification_emoji_corn">Majskolv</string>
|
||||
<string name="verification_emoji_corn">Majs</string>
|
||||
<string name="verification_emoji_pizza">Pizza</string>
|
||||
<string name="verification_emoji_cake">Tårta</string>
|
||||
<string name="verification_emoji_heart">Hjärta</string>
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="verification_emoji_umbrella">Paraply</string>
|
||||
<string name="verification_emoji_hourglass">Timglas</string>
|
||||
<string name="verification_emoji_clock">Klocka</string>
|
||||
<string name="verification_emoji_gift">Paket</string>
|
||||
<string name="verification_emoji_gift">Present</string>
|
||||
<string name="verification_emoji_light_bulb">Lampa</string>
|
||||
<string name="verification_emoji_book">Bok</string>
|
||||
<string name="verification_emoji_pencil">Penna</string>
|
||||
@@ -52,7 +52,7 @@
|
||||
<string name="verification_emoji_hammer">Hammare</string>
|
||||
<string name="verification_emoji_telephone">Telefon</string>
|
||||
<string name="verification_emoji_flag">Flagga</string>
|
||||
<string name="verification_emoji_train">Ånglok</string>
|
||||
<string name="verification_emoji_train">Tåg</string>
|
||||
<string name="verification_emoji_bicycle">Cykel</string>
|
||||
<string name="verification_emoji_aeroplane">Flygplan</string>
|
||||
<string name="verification_emoji_rocket">Raket</string>
|
||||
|
@@ -7,4 +7,62 @@
|
||||
<string name="verification_emoji_horse">马</string>
|
||||
<string name="verification_emoji_unicorn">独角兽</string>
|
||||
<string name="verification_emoji_pig">猪</string>
|
||||
<string name="verification_emoji_elephant">大象</string>
|
||||
<string name="verification_emoji_rabbit">兔子</string>
|
||||
<string name="verification_emoji_panda">熊猫</string>
|
||||
<string name="verification_emoji_rooster">公鸡</string>
|
||||
<string name="verification_emoji_penguin">企鹅</string>
|
||||
<string name="verification_emoji_turtle">乌龟</string>
|
||||
<string name="verification_emoji_fish">鱼</string>
|
||||
<string name="verification_emoji_octopus">章鱼</string>
|
||||
<string name="verification_emoji_butterfly">蝴蝶</string>
|
||||
<string name="verification_emoji_flower">花</string>
|
||||
<string name="verification_emoji_tree">树</string>
|
||||
<string name="verification_emoji_cactus">仙人掌</string>
|
||||
<string name="verification_emoji_mushroom">蘑菇</string>
|
||||
<string name="verification_emoji_globe">地球</string>
|
||||
<string name="verification_emoji_moon">月亮</string>
|
||||
<string name="verification_emoji_cloud">云</string>
|
||||
<string name="verification_emoji_fire">火</string>
|
||||
<string name="verification_emoji_banana">香蕉</string>
|
||||
<string name="verification_emoji_apple">苹果</string>
|
||||
<string name="verification_emoji_strawberry">草莓</string>
|
||||
<string name="verification_emoji_corn">玉米</string>
|
||||
<string name="verification_emoji_pizza">披萨</string>
|
||||
<string name="verification_emoji_cake">蛋糕</string>
|
||||
<string name="verification_emoji_heart">心</string>
|
||||
<string name="verification_emoji_smiley">笑脸</string>
|
||||
<string name="verification_emoji_robot">机器人</string>
|
||||
<string name="verification_emoji_hat">帽子</string>
|
||||
<string name="verification_emoji_glasses">眼镜</string>
|
||||
<string name="verification_emoji_spanner">扳手</string>
|
||||
<string name="verification_emoji_santa">圣诞老人</string>
|
||||
<string name="verification_emoji_thumbs_up">赞</string>
|
||||
<string name="verification_emoji_umbrella">伞</string>
|
||||
<string name="verification_emoji_hourglass">沙漏</string>
|
||||
<string name="verification_emoji_clock">时钟</string>
|
||||
<string name="verification_emoji_gift">礼物</string>
|
||||
<string name="verification_emoji_light_bulb">灯泡</string>
|
||||
<string name="verification_emoji_book">书</string>
|
||||
<string name="verification_emoji_pencil">铅笔</string>
|
||||
<string name="verification_emoji_paperclip">回形针</string>
|
||||
<string name="verification_emoji_scissors">剪刀</string>
|
||||
<string name="verification_emoji_lock">锁</string>
|
||||
<string name="verification_emoji_key">钥匙</string>
|
||||
<string name="verification_emoji_hammer">锤子</string>
|
||||
<string name="verification_emoji_telephone">电话</string>
|
||||
<string name="verification_emoji_flag">旗帜</string>
|
||||
<string name="verification_emoji_train">火车</string>
|
||||
<string name="verification_emoji_bicycle">自行车</string>
|
||||
<string name="verification_emoji_aeroplane">飞机</string>
|
||||
<string name="verification_emoji_rocket">火箭</string>
|
||||
<string name="verification_emoji_trophy">奖杯</string>
|
||||
<string name="verification_emoji_ball">球</string>
|
||||
<string name="verification_emoji_guitar">吉他</string>
|
||||
<string name="verification_emoji_trumpet">喇叭</string>
|
||||
<string name="verification_emoji_bell">铃铛</string>
|
||||
<string name="verification_emoji_anchor">锚</string>
|
||||
<string name="verification_emoji_headphones">耳机</string>
|
||||
<string name="verification_emoji_folder">文件夹</string>
|
||||
<string name="verification_emoji_pin">图钉</string>
|
||||
</resources>
|
||||
|
@@ -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>
|
||||
|
@@ -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/*"
|
||||
|
@@ -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 = "*/*"
|
||||
|
@@ -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/*"
|
||||
|
@@ -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/*"
|
||||
|
@@ -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
69
tools/hs_diag.py
Executable 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")
|
@@ -17,7 +17,7 @@ androidExtensions {
|
||||
// Note: 2 digits max for each value
|
||||
ext.versionMajor = 1
|
||||
ext.versionMinor = 0
|
||||
ext.versionPatch = 7
|
||||
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'
|
||||
@@ -352,7 +374,7 @@ dependencies {
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
implementation 'com.google.android:flexbox:1.1.1'
|
||||
implementation "androidx.autofill:autofill:$autofill_version"
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta9'
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10'
|
||||
|
||||
// Custom Tab
|
||||
implementation 'androidx.browser:browser:1.2.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'
|
||||
|
207
vector/src/androidTest/java/im/vector/app/EspressoExt.kt
Normal file
207
vector/src/androidTest/java/im/vector/app/EspressoExt.kt
Normal 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
|
||||
}
|
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal file
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal 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()))
|
||||
}
|
||||
}
|
||||
}
|
@@ -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())
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user