mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
74 Commits
v1.0.8
...
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 | ||
|
62f620f79b | ||
|
3442ebc1c3 |
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
|
30
CHANGES.md
30
CHANGES.md
@@ -1,3 +1,33 @@
|
||||
Changes in Element 1.0.9 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
||||
|
||||
Improvements 🙌:
|
||||
- Wording differentiation for direct rooms (#2176)
|
||||
- PIN code: request PIN code if phone has been locked
|
||||
- Small optimisation of scrolling experience in timeline (#2114)
|
||||
- Allow user to reset cross signing if he has no way to recover (#2052)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Improve support for image/audio/video/file selection with intent changes (#1376)
|
||||
- Fix Splash layout on small screens
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
- Use Update Gradle Wrapper Action
|
||||
- Updates Gradle Wrapper from 5.6.4 to 6.6.1. (#2193)
|
||||
|
||||
Other changes:
|
||||
- Added registration/verification automated UI tests
|
||||
- Create a script to help getting public information form any homeserver
|
||||
|
||||
Changes in Element 1.0.8 (2020-09-25)
|
||||
===================================================
|
||||
|
||||
|
107
docs/ui-tests.md
Normal file
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()
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
@@ -653,12 +654,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
/**
|
||||
* Encrypt an event content according to the configuration of the room.
|
||||
*
|
||||
* @param eventId the identifier of the event
|
||||
* @param eventContent the content of the event.
|
||||
* @param eventType the type of the event.
|
||||
* @param roomId the room identifier the event will be sent.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
override fun encryptEventContent(eventContent: Content,
|
||||
override fun encryptEventContent(eventId: String,
|
||||
eventContent: Content,
|
||||
eventType: String,
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||
@@ -667,7 +670,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
|
||||
// internalStart(false)
|
||||
// }
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
|
||||
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_GET_USERS)
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_GET_USERS)
|
||||
|
||||
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_SET_ROOM_ENCRYPTION)
|
||||
var alg = roomEncryptorsStore.get(roomId)
|
||||
if (alg == null) {
|
||||
val algorithm = getEncryptionAlgorithm(roomId)
|
||||
@@ -677,12 +685,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.ENCRYPT_SET_ROOM_ENCRYPTION)
|
||||
val safeAlgorithm = alg
|
||||
if (safeAlgorithm != null) {
|
||||
val t0 = System.currentTimeMillis()
|
||||
Timber.v("## CRYPTO | encryptEventContent() starts")
|
||||
runCatching {
|
||||
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
val content = safeAlgorithm.encryptEventContent(eventId, eventContent, eventType, userIds)
|
||||
Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
|
||||
}.foldToCallback(callback)
|
||||
@@ -803,17 +812,17 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
onKeyWithHeldReceived(event)
|
||||
}
|
||||
else -> {
|
||||
@@ -892,7 +901,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
true
|
||||
}
|
||||
@@ -1351,9 +1360,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
|
||||
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
|
||||
}
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
* ========================================================================================== */
|
||||
|
||||
@VisibleForTesting
|
||||
val cryptoStoreForTesting = cryptoStore
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 ->
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
@@ -65,7 +66,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
|
||||
|
||||
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.GET_UP_TO_DATE_ECHO)
|
||||
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
|
||||
SendPerformanceProfiler.stopStage(params.eventId, SendPerformanceProfiler.Stages.GET_UP_TO_DATE_ECHO)
|
||||
|
||||
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.ENCRYPT_WORKER)
|
||||
if (localEvent?.eventId == null) {
|
||||
return Result.success()
|
||||
}
|
||||
@@ -86,7 +91,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
var result: MXEncryptEventContentResult? = null
|
||||
try {
|
||||
result = awaitCallback {
|
||||
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
|
||||
crypto.encryptEventContent(localEvent.eventId, localMutableContent, localEvent.type, localEvent.roomId!!, it)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
error = throwable
|
||||
@@ -122,7 +127,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
localEcho.setDecryptionResult(it)
|
||||
}
|
||||
}
|
||||
|
||||
SendPerformanceProfiler.stopStage(params.eventId, SendPerformanceProfiler.Stages.ENCRYPT_WORKER)
|
||||
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
} else {
|
||||
|
@@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
@@ -62,6 +63,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
||||
if (event.eventId == null) {
|
||||
throw IllegalStateException("You should have set an eventId for your event")
|
||||
}
|
||||
// SendPerformanceProfiler.startStage(event.eventId, SendPerformanceProfiler.Stages.LOCAL_ECHO)
|
||||
val timelineEventEntity = realmSessionProvider.withRealm { realm ->
|
||||
val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis())
|
||||
val roomMemberHelper = RoomMemberHelper(realm, roomId)
|
||||
@@ -86,6 +88,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@asyncTransaction
|
||||
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
|
||||
roomSummaryUpdater.updateSendingInformation(realm, roomId)
|
||||
//SendPerformanceProfiler.stopStage(event.eventId, SendPerformanceProfiler.Stages.LOCAL_ECHO)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 ?: "")
|
||||
|
@@ -24,6 +24,7 @@ import io.realm.RealmConfiguration
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
@@ -62,6 +63,7 @@ internal class SendEventWorker(context: Context,
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
SendPerformanceProfiler.startStage(params.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
|
||||
val event = localEchoRepository.getUpToDateEcho(params.eventId)
|
||||
if (event?.eventId == null || event.roomId == null) {
|
||||
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
|
||||
@@ -87,6 +89,7 @@ internal class SendEventWorker(context: Context,
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
|
||||
return try {
|
||||
sendEvent(event.eventId, event.roomId, event.type, event.content)
|
||||
SendPerformanceProfiler.stopStage(event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
|
||||
Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
|
||||
@@ -106,9 +109,11 @@ internal class SendEventWorker(context: Context,
|
||||
|
||||
private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENDING)
|
||||
SendPerformanceProfiler.startStage(eventId, SendPerformanceProfiler.Stages.SEND_REQUEST)
|
||||
executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(eventId, roomId, type, content)
|
||||
}
|
||||
SendPerformanceProfiler.stopStage(eventId, SendPerformanceProfiler.Stages.SEND_REQUEST)
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENT)
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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 = 8
|
||||
ext.versionPatch = 9
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
@@ -172,6 +172,19 @@ android {
|
||||
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
|
||||
}
|
||||
}
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
// Disables animations during instrumented tests you run from the command line…
|
||||
// This property does not affect tests that you run using Android Studio.”
|
||||
animationsDisabled = true
|
||||
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -281,6 +294,11 @@ dependencies {
|
||||
def arch_version = '2.1.0'
|
||||
def lifecycle_version = '2.2.0'
|
||||
|
||||
// Tests
|
||||
def kluent_version = '1.44'
|
||||
def androidxTest_version = '1.3.0'
|
||||
def espresso_version = '3.3.0'
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
implementation project(":diff-match-patch")
|
||||
@@ -316,6 +334,9 @@ dependencies {
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||
|
||||
debugImplementation 'com.nikitakozlov.pury:pury:1.1.0'
|
||||
releaseImplementation 'com.nikitakozlov.pury:pury-no-op:1.1.0'
|
||||
|
||||
// rx
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
@@ -326,6 +347,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
|
||||
|
||||
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
||||
implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version"
|
||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
|
||||
implementation 'com.airbnb.android:mvrx:1.3.0'
|
||||
@@ -421,19 +443,21 @@ dependencies {
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
// Activate when you want to check for leaks, from time to time.
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "androidx.test:core:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:runner:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:rules:$androidxTest_version"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
|
||||
androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
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() {
|
||||
|
@@ -27,6 +27,7 @@ import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||
@@ -530,6 +531,11 @@ interface FragmentModule {
|
||||
@FragmentKey(SharedSecuredStorageKeyFragment::class)
|
||||
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SharedSecuredStorageResetAllFragment::class)
|
||||
fun bindSharedSecuredStorageResetAllFragment(fragment: SharedSecuredStorageResetAllFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SetIdentityServerFragment::class)
|
||||
|
@@ -17,6 +17,7 @@ package im.vector.app.core.platform
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@@ -86,6 +87,24 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||
|
||||
open val showExpanded = false
|
||||
|
||||
interface ResultListener {
|
||||
fun onBottomSheetResult(resultCode: Int, data: Any?)
|
||||
|
||||
companion object {
|
||||
const val RESULT_OK = 1
|
||||
const val RESULT_CANCEL = 0
|
||||
}
|
||||
}
|
||||
|
||||
var resultListener : ResultListener? = null
|
||||
var bottomSheetResult: Int = ResultListener.RESULT_CANCEL
|
||||
var bottomSheetResultData: Any? = null
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
resultListener?.onBottomSheetResult(bottomSheetResult, bottomSheetResultData)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(getLayoutResId(), container, false)
|
||||
unBinder = ButterKnife.bind(this, view)
|
||||
|
@@ -24,6 +24,7 @@ import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
@@ -107,6 +108,12 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||
leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
||||
}
|
||||
|
||||
var titleTextColor: Int? = null
|
||||
set(value) {
|
||||
field = value
|
||||
value?.let { actionTextView.setTextColor(it) }
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_verification_action, this)
|
||||
ButterKnife.bind(this)
|
||||
@@ -120,6 +127,7 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||
rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
|
||||
|
||||
tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ContextCompat.getColor(context, R.color.riotx_accent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -55,6 +55,10 @@ fun isAirplaneModeOn(context: Context): Boolean {
|
||||
return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
|
||||
}
|
||||
|
||||
fun isAnimationDisabled(context: Context): Boolean {
|
||||
return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* display the system dialog for granting this permission. If previously granted, the
|
||||
* system will not show it (so you should call this method).
|
||||
|
@@ -28,6 +28,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction {
|
||||
object Cancel : SharedSecureStorageAction()
|
||||
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
|
||||
data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction()
|
||||
object ForgotResetAll : SharedSecureStorageAction()
|
||||
object DoResetAll : SharedSecureStorageAction()
|
||||
}
|
||||
|
||||
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
||||
@@ -40,4 +42,5 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
||||
object ShowModalLoading : SharedSecureStorageViewEvent()
|
||||
object HideModalLoading : SharedSecureStorageViewEvent()
|
||||
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
|
||||
object ShowResetBottomSheet : SharedSecureStorageViewEvent()
|
||||
}
|
||||
|
@@ -31,12 +31,14 @@ import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
class SharedSecureStorageActivity : SimpleFragmentActivity(), VectorBaseBottomSheetDialogFragment.ResultListener {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
@@ -69,18 +71,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
|
||||
private fun renderState(state: SharedSecureStorageViewState) {
|
||||
if (!state.ready) return
|
||||
val fragment = if (state.hasPassphrase) {
|
||||
if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class
|
||||
} else SharedSecuredStorageKeyFragment::class
|
||||
val fragment =
|
||||
when (state.step) {
|
||||
SharedSecureStorageViewState.Step.EnterPassphrase -> SharedSecuredStoragePassphraseFragment::class
|
||||
SharedSecureStorageViewState.Step.EnterKey -> SharedSecuredStorageKeyFragment::class
|
||||
SharedSecureStorageViewState.Step.ResetAll -> SharedSecuredStorageResetAllFragment::class
|
||||
}
|
||||
|
||||
showFragment(fragment, Bundle())
|
||||
}
|
||||
|
||||
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.Dismiss -> {
|
||||
is SharedSecureStorageViewEvent.Dismiss -> {
|
||||
finish()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.Error -> {
|
||||
is SharedSecureStorageViewEvent.Error -> {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.dialog_title_error))
|
||||
.setMessage(it.message)
|
||||
@@ -92,21 +98,31 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
}
|
||||
.show()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.ShowModalLoading -> {
|
||||
is SharedSecureStorageViewEvent.ShowModalLoading -> {
|
||||
showWaitingView()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.HideModalLoading -> {
|
||||
is SharedSecureStorageViewEvent.HideModalLoading -> {
|
||||
hideWaitingView()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
|
||||
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
|
||||
updateWaitingView(it.waitingData)
|
||||
}
|
||||
is SharedSecureStorageViewEvent.FinishSuccess -> {
|
||||
is SharedSecureStorageViewEvent.FinishSuccess -> {
|
||||
val dataResult = Intent()
|
||||
dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
|
||||
setResult(Activity.RESULT_OK, dataResult)
|
||||
finish()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.ShowResetBottomSheet -> {
|
||||
navigator.open4SSetup(this, SetupMode.HARD_RESET)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachFragment(fragment: Fragment) {
|
||||
super.onAttachFragment(fragment)
|
||||
if (fragment is VectorBaseBottomSheetDialogFragment) {
|
||||
fragment.resultListener = this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +140,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
|
||||
const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET"
|
||||
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
|
||||
|
||||
fun newIntent(context: Context,
|
||||
@@ -140,4 +157,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBottomSheetResult(resultCode: Int, data: Any?) {
|
||||
if (resultCode == VectorBaseBottomSheetDialogFragment.ResultListener.RESULT_OK) {
|
||||
// the 4S has been reset
|
||||
setResult(Activity.RESULT_OK, Intent().apply { putExtra(EXTRA_DATA_RESET, true) })
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,9 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
|
||||
@@ -40,19 +43,26 @@ import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
|
||||
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
data class SharedSecureStorageViewState(
|
||||
val ready: Boolean = false,
|
||||
val hasPassphrase: Boolean = true,
|
||||
val useKey: Boolean = false,
|
||||
val passphraseVisible: Boolean = false,
|
||||
val checkingSSSSAction: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
val checkingSSSSAction: Async<Unit> = Uninitialized,
|
||||
val step: Step = Step.EnterPassphrase,
|
||||
val activeDeviceCount: Int = 0,
|
||||
val showResetAllAction: Boolean = false,
|
||||
val userId: String = ""
|
||||
) : MvRxState {
|
||||
enum class Step {
|
||||
EnterPassphrase,
|
||||
EnterKey,
|
||||
ResetAll
|
||||
}
|
||||
}
|
||||
|
||||
class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: SharedSecureStorageViewState,
|
||||
@@ -67,6 +77,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
setState {
|
||||
copy(userId = session.myUserId)
|
||||
}
|
||||
val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
|
||||
if (!isValid) {
|
||||
_viewEvents.post(
|
||||
@@ -86,20 +100,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
if (info.content.passphrase != null) {
|
||||
setState {
|
||||
copy(
|
||||
ready = true,
|
||||
hasPassphrase = true,
|
||||
useKey = false
|
||||
ready = true,
|
||||
step = SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
hasPassphrase = false,
|
||||
ready = true,
|
||||
hasPassphrase = false
|
||||
step = SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.rx()
|
||||
.liveUserCryptoDevices(session.myUserId)
|
||||
.distinctUntilChanged()
|
||||
.execute {
|
||||
copy(
|
||||
activeDeviceCount = it.invoke()?.size ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: SharedSecureStorageAction) = withState {
|
||||
@@ -110,27 +134,52 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
SharedSecureStorageAction.UseKey -> handleUseKey()
|
||||
is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action)
|
||||
SharedSecureStorageAction.Back -> handleBack()
|
||||
SharedSecureStorageAction.ForgotResetAll -> handleResetAll()
|
||||
SharedSecureStorageAction.DoResetAll -> handleDoResetAll()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleDoResetAll() {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet)
|
||||
}
|
||||
|
||||
private fun handleResetAll() {
|
||||
setState {
|
||||
copy(
|
||||
step = SharedSecureStorageViewState.Step.ResetAll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUseKey() {
|
||||
setState {
|
||||
copy(
|
||||
useKey = true
|
||||
step = SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBack() = withState { state ->
|
||||
if (state.checkingSSSSAction is Loading) return@withState // ignore
|
||||
if (state.hasPassphrase && state.useKey) {
|
||||
setState {
|
||||
copy(
|
||||
useKey = false
|
||||
)
|
||||
when (state.step) {
|
||||
SharedSecureStorageViewState.Step.EnterKey -> {
|
||||
setState {
|
||||
copy(
|
||||
step = SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
)
|
||||
}
|
||||
}
|
||||
SharedSecureStorageViewState.Step.ResetAll -> {
|
||||
setState {
|
||||
copy(
|
||||
step = if (state.hasPassphrase) SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
else SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +207,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) }
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@@ -27,9 +27,9 @@ import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.startImportTextFromFileIntent
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -63,6 +63,10 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment
|
||||
|
||||
ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) }
|
||||
|
||||
ssss_key_reset.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
|
||||
}
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.KeyInlineError -> {
|
||||
|
@@ -74,6 +74,10 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_passphrase_reset.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
|
||||
}
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.InlineError -> {
|
||||
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.crypto.quads
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_reset_all.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class SharedSecuredStorageResetAllFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_ssss_reset_all
|
||||
|
||||
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
ssss_reset_button_reset.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.DoResetAll)
|
||||
}
|
||||
|
||||
ssss_reset_button_cancel.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.Back)
|
||||
}
|
||||
|
||||
ssss_reset_other_devices.debouncedClicks {
|
||||
withState(sharedViewModel) {
|
||||
DeviceListBottomSheet.newInstance(it.userId, false).show(childFragmentManager, "DEV_LIST")
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.subscribe(this) { state ->
|
||||
ssss_reset_other_devices.setTextOrHide(
|
||||
state.activeDeviceCount
|
||||
.takeIf { it > 0 }
|
||||
?.let { resources.getQuantityString(R.plurals.secure_backup_reset_devices_you_can_verify, it, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -45,8 +45,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val initCrossSigningOnly: Boolean,
|
||||
val forceReset4S: Boolean
|
||||
val setUpMode: SetupMode = SetupMode.NORMAL
|
||||
) : Parcelable
|
||||
|
||||
override val showExpanded = true
|
||||
@@ -66,7 +65,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.observeViewEvents { event ->
|
||||
when (event) {
|
||||
is BootstrapViewEvents.Dismiss -> dismiss()
|
||||
is BootstrapViewEvents.Dismiss -> {
|
||||
bottomSheetResult = if (event.success) ResultListener.RESULT_OK else ResultListener.RESULT_CANCEL
|
||||
dismiss()
|
||||
}
|
||||
is BootstrapViewEvents.ModalError -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
@@ -90,6 +92,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
.setMessage(R.string.bootstrap_cancel_text)
|
||||
.setPositiveButton(R.string._continue, null)
|
||||
.setNegativeButton(R.string.skip) { _, _ ->
|
||||
bottomSheetResult = ResultListener.RESULT_CANCEL
|
||||
dismiss()
|
||||
}
|
||||
.show()
|
||||
@@ -181,16 +184,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
|
||||
BootstrapBottomSheet().apply {
|
||||
fun show(fragmentManager: FragmentManager, mode: SetupMode): BootstrapBottomSheet {
|
||||
return BootstrapBottomSheet().apply {
|
||||
isCancelable = false
|
||||
arguments = Bundle().apply {
|
||||
this.putParcelable(EXTRA_ARGS, Args(
|
||||
initCrossSigningOnly,
|
||||
forceReset4S
|
||||
))
|
||||
this.putParcelable(EXTRA_ARGS, Args(setUpMode = mode))
|
||||
}
|
||||
}.show(fragmentManager, "BootstrapBottomSheet")
|
||||
}.also {
|
||||
it.show(fragmentManager, "BootstrapBottomSheet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -69,10 +69,10 @@ interface BootstrapProgressListener {
|
||||
|
||||
data class Params(
|
||||
val userPasswordAuth: UserPasswordAuth? = null,
|
||||
val initOnlyCrossSigning: Boolean = false,
|
||||
val progressListener: BootstrapProgressListener? = null,
|
||||
val passphrase: String?,
|
||||
val keySpec: SsssKeySpec? = null
|
||||
val keySpec: SsssKeySpec? = null,
|
||||
val setupMode: SetupMode
|
||||
)
|
||||
|
||||
// TODO Rename to CreateServerRecovery
|
||||
@@ -84,9 +84,13 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
override suspend fun execute(params: Params): BootstrapResult {
|
||||
val crossSigningService = session.cryptoService().crossSigningService()
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...")
|
||||
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Starting...")
|
||||
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
|
||||
if (!crossSigningService.isCrossSigningInitialized()) {
|
||||
|
||||
val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized()
|
||||
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown())
|
||||
|| (params.setupMode == SetupMode.HARD_RESET)
|
||||
if (shouldSetCrossSigning) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
@@ -99,7 +103,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
}
|
||||
if (params.initOnlyCrossSigning) {
|
||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||
return BootstrapResult.SuccessCrossSigningOnly
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
@@ -107,7 +111,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
}
|
||||
} else {
|
||||
Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
|
||||
if (params.initOnlyCrossSigning) {
|
||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||
// not sure how this can happen??
|
||||
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
|
||||
}
|
||||
@@ -236,7 +240,13 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
val serverVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}
|
||||
if (serverVersion == null) {
|
||||
|
||||
val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
|
||||
val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version
|
||||
val shouldCreateKeyBackup = serverVersion == null
|
||||
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown)
|
||||
|| (params.setupMode == SetupMode.HARD_RESET)
|
||||
if (shouldCreateKeyBackup) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
@@ -260,16 +270,15 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
} else {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
|
||||
// ensure we store existing backup secret if we have it!
|
||||
val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
|
||||
if (knownSecret != null && knownSecret.version == serverVersion.version) {
|
||||
if (isMegolmBackupSecretKnown) {
|
||||
// check it matches
|
||||
val isValid = awaitCallback<Boolean> {
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey, it)
|
||||
}
|
||||
if (isValid) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
|
||||
awaitCallback<Unit> {
|
||||
extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
ssssService.storeSecret(
|
||||
KEYBACKUP_SECRET_SSSS_NAME,
|
||||
secret,
|
||||
@@ -286,7 +295,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
||||
}
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
|
||||
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Finished")
|
||||
return BootstrapResult.Success(keyInfo)
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,8 @@ import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
||||
@@ -41,8 +43,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
|
||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
@@ -69,46 +69,52 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
|
||||
init {
|
||||
|
||||
if (args.forceReset4S) {
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
|
||||
}
|
||||
} else if (args.initCrossSigningOnly) {
|
||||
// Go straight to account password
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
} else {
|
||||
// need to check if user have an existing keybackup
|
||||
setState {
|
||||
copy(step = BootstrapStep.CheckingMigration)
|
||||
}
|
||||
|
||||
// We need to check if there is an existing backup
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
when (args.setUpMode) {
|
||||
SetupMode.PASSPHRASE_RESET,
|
||||
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
|
||||
SetupMode.HARD_RESET -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
|
||||
}
|
||||
if (version == null) {
|
||||
// we just resume plain bootstrap
|
||||
doesKeyBackupExist = false
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
SetupMode.CROSS_SIGNING_ONLY -> {
|
||||
// Go straight to account password
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
}
|
||||
SetupMode.NORMAL -> {
|
||||
// need to check if user have an existing keybackup
|
||||
setState {
|
||||
copy(step = BootstrapStep.CheckingMigration)
|
||||
}
|
||||
|
||||
// We need to check if there is an existing backup
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}
|
||||
} else {
|
||||
// we need to get existing backup passphrase/key and convert to SSSS
|
||||
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||
}
|
||||
if (keyVersion == null) {
|
||||
// strange case... just finish?
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
} else {
|
||||
doesKeyBackupExist = true
|
||||
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
if (version == null) {
|
||||
// we just resume plain bootstrap
|
||||
doesKeyBackupExist = false
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
} else {
|
||||
// we need to get existing backup passphrase/key and convert to SSSS
|
||||
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||
}
|
||||
if (keyVersion == null) {
|
||||
// strange case... just finish?
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
|
||||
} else {
|
||||
doesKeyBackupExist = true
|
||||
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +240,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
BootstrapActions.Completed -> {
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||
}
|
||||
BootstrapActions.GoToCompleted -> {
|
||||
setState {
|
||||
@@ -395,16 +401,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
bootstrapTask.invoke(this,
|
||||
Params(
|
||||
userPasswordAuth = userPasswordAuth,
|
||||
initOnlyCrossSigning = args.initCrossSigningOnly,
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase,
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
|
||||
setupMode = args.setUpMode
|
||||
)
|
||||
) { bootstrapResult ->
|
||||
when (bootstrapResult) {
|
||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||
// TPD
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||
}
|
||||
is BootstrapResult.Success -> {
|
||||
setState {
|
||||
@@ -428,7 +434,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
@@ -522,7 +528,13 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
BootstrapStep.CheckingMigration -> Unit
|
||||
is BootstrapStep.FirstForm -> {
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
_viewEvents.post(
|
||||
when (args.setUpMode) {
|
||||
SetupMode.CROSS_SIGNING_ONLY,
|
||||
SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap()
|
||||
else -> BootstrapViewEvents.Dismiss(success = false)
|
||||
}
|
||||
)
|
||||
}
|
||||
is BootstrapStep.GetBackupSecretForMigration -> {
|
||||
setState {
|
||||
@@ -558,7 +570,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
|
||||
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
|
||||
?: BootstrapBottomSheet.Args(SetupMode.CROSS_SIGNING_ONLY)
|
||||
return fragment.bootstrapViewModelFactory.create(state, args)
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ package im.vector.app.features.crypto.recover
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class BootstrapViewEvents : VectorViewEvents {
|
||||
object Dismiss : BootstrapViewEvents()
|
||||
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
|
||||
data class ModalError(val error: String) : BootstrapViewEvents()
|
||||
object RecoveryKeySaved: BootstrapViewEvents()
|
||||
data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents()
|
||||
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.crypto.recover
|
||||
|
||||
enum class SetupMode {
|
||||
|
||||
/**
|
||||
* Only setup cross signing, no 4S or megolm backup
|
||||
*/
|
||||
CROSS_SIGNING_ONLY,
|
||||
|
||||
/**
|
||||
* Normal setup mode.
|
||||
*/
|
||||
NORMAL,
|
||||
|
||||
/**
|
||||
* Only reset the 4S passphrase/key, but do not touch
|
||||
* to existing cross-signing or megolm backup
|
||||
* It take the local known secrets and put them in 4S
|
||||
*/
|
||||
PASSPHRASE_RESET,
|
||||
|
||||
/**
|
||||
* Resets the passphrase/key, and all missing secrets
|
||||
* are re-created. Meaning that if cross signing is setup and the secrets
|
||||
* keys are not known, cross signing will be reset (if secret is known we just keep same cross signing)
|
||||
* Same apply to megolm
|
||||
*/
|
||||
PASSPHRASE_AND_NEEDED_SECRETS_RESET,
|
||||
|
||||
/**
|
||||
* Resets the passphrase/key, cross signing and megolm backup
|
||||
*/
|
||||
HARD_RESET
|
||||
}
|
@@ -31,4 +31,5 @@ sealed class VerificationAction : VectorViewModelAction {
|
||||
object SkipVerification : VerificationAction()
|
||||
object VerifyFromPassphrase : VerificationAction()
|
||||
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
|
||||
object SecuredStorageHasBeenReset : VerificationAction()
|
||||
}
|
||||
|
@@ -48,6 +48,7 @@ import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrS
|
||||
import im.vector.app.features.crypto.verification.request.VerificationRequestFragment
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
@@ -55,7 +56,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
@@ -76,6 +76,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
@@ -146,8 +147,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
|
||||
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
|
||||
viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
|
||||
val result = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
|
||||
val reset = data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
|
||||
if (result != null) {
|
||||
viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
|
||||
} else if (reset) {
|
||||
// all have been reset, so we are verified?
|
||||
viewModel.handle(VerificationAction.SecuredStorageHasBeenReset)
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -182,6 +188,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
if (state.quadSHasBeenReset) {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||
isSuccessFull = true,
|
||||
isMe = true,
|
||||
cancelReason = null
|
||||
))
|
||||
})
|
||||
return@withState
|
||||
}
|
||||
|
||||
if (state.userThinkItsNotHim) {
|
||||
otherUserNameText.text = getString(R.string.dialog_title_warning)
|
||||
showFragment(VerificationNotMeFragment::class, Bundle())
|
||||
@@ -356,6 +373,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
|
||||
return VerificationBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
|
@@ -31,6 +31,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
@@ -74,7 +75,8 @@ data class VerificationBottomSheetViewState(
|
||||
val currentDeviceCanCrossSign: Boolean = false,
|
||||
val userWantsToCancel: Boolean = false,
|
||||
val userThinkItsNotHim: Boolean = false,
|
||||
val quadSContainsSecrets: Boolean = true
|
||||
val quadSContainsSecrets: Boolean = true,
|
||||
val quadSHasBeenReset: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
@@ -349,6 +351,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
is VerificationAction.GotResultFromSsss -> {
|
||||
handleSecretBackFromSSSS(action)
|
||||
}
|
||||
VerificationAction.SecuredStorageHasBeenReset -> {
|
||||
if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) {
|
||||
setState {
|
||||
copy(quadSHasBeenReset = true)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@@ -393,7 +403,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun tentativeRestoreBackup(res: Map<String, String>?) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
||||
Timber.v("## Keybackup secret not restored from SSSS")
|
||||
|
@@ -53,6 +53,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.OnModelBuildFinishedListener
|
||||
import com.airbnb.epoxy.addGlidePreloader
|
||||
import com.airbnb.epoxy.glidePreloader
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
@@ -75,6 +77,7 @@ import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.extensions.showKeyboard
|
||||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
@@ -218,7 +221,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider,
|
||||
private val notificationUtils: NotificationUtils,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
@@ -700,7 +704,13 @@ class RoomDetailFragment @Inject constructor(
|
||||
// safeStartCall(it, isVideoCall)
|
||||
// }
|
||||
} else if (!state.isAllowedToStartWebRTCCall) {
|
||||
showDialogWithMessage(getString(R.string.no_permissions_to_start_webrtc_call))
|
||||
showDialogWithMessage(getString(
|
||||
if (state.isDm()) {
|
||||
R.string.no_permissions_to_start_webrtc_call_in_direct_room
|
||||
} else {
|
||||
R.string.no_permissions_to_start_webrtc_call
|
||||
})
|
||||
)
|
||||
} else {
|
||||
safeStartCall(isVideoCall)
|
||||
}
|
||||
@@ -710,7 +720,13 @@ class RoomDetailFragment @Inject constructor(
|
||||
// can you add widgets??
|
||||
if (!state.isAllowedToManageWidgets) {
|
||||
// You do not have permission to start a conference call in this room
|
||||
showDialogWithMessage(getString(R.string.no_permissions_to_start_conf_call))
|
||||
showDialogWithMessage(getString(
|
||||
if (state.isDm()) {
|
||||
R.string.no_permissions_to_start_conf_call_in_direct_room
|
||||
} else {
|
||||
R.string.no_permissions_to_start_conf_call
|
||||
}
|
||||
))
|
||||
} else {
|
||||
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
|
||||
// A conference is already in progress!
|
||||
@@ -921,6 +937,16 @@ class RoomDetailFragment @Inject constructor(
|
||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||
touchHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
recyclerView.addGlidePreloader(
|
||||
epoxyController = timelineEventController,
|
||||
requestManager = GlideApp.with(this),
|
||||
preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ ->
|
||||
imageContentRenderer.createGlideRequest(
|
||||
epoxyModel.mediaData,
|
||||
ImageContentRenderer.Mode.THUMBNAIL,
|
||||
requestManager as GlideRequests
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateJumpToReadMarkerViewVisibility() {
|
||||
|
@@ -94,6 +94,7 @@ import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.room.send.SendPerformanceProfiler
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
@@ -551,7 +552,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) {
|
||||
SendPerformanceProfiler.startProfiling(it.eventId!!)
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
@@ -890,6 +893,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (action.event.root.sendState.isSent()) { // ignore pending/local events
|
||||
action.event.root.unsignedData?.transactionId?.also {
|
||||
SendPerformanceProfiler.stopProfiling(it)
|
||||
}
|
||||
visibleEventsObservable.accept(action)
|
||||
}
|
||||
// We need to update this with the related m.replace also (to move read receipt)
|
||||
|
@@ -77,4 +77,6 @@ data class RoomDetailViewState(
|
||||
// Also highlight the target event, if any
|
||||
highlightedEventId = args.eventId
|
||||
)
|
||||
|
||||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
@@ -50,18 +51,28 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||
private val session: Session,
|
||||
@TimelineEventControllerHandler
|
||||
private val backgroundHandler: Handler
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
@@ -113,9 +124,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var hasReachedInvite: Boolean = false
|
||||
private var hasUTD: Boolean = false
|
||||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var previousModelsSize = 0
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
@@ -163,6 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
init {
|
||||
isDebugLoggingEnabled = BuildConfig.DEBUG
|
||||
addInterceptor(this)
|
||||
requestModelBuild()
|
||||
}
|
||||
@@ -191,6 +206,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
models.add(position, readMarker)
|
||||
}
|
||||
}
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
|
||||
models.add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
models.add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
@@ -241,7 +279,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return
|
||||
}
|
||||
// Avoid displaying two loaders if there is no elements between them
|
||||
val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty()
|
||||
// We can hide the loader but still add the item to controller so it can trigger backwards pagination
|
||||
@@ -301,6 +341,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
|
||||
hasUTD = false
|
||||
hasReachedInvite = false
|
||||
|
||||
if (modelCache.isEmpty()) {
|
||||
return
|
||||
}
|
||||
@@ -316,13 +359,21 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextOrNull(currentPosition)
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
||||
}
|
||||
updateUTDStates(event, nextEvent)
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val addDaySeparator = if (hasReachedInvite && hasUTD) {
|
||||
true
|
||||
} else {
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
date.toLocalDate() != nextDate?.toLocalDate()
|
||||
}
|
||||
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
|
||||
nextEvent = nextEvent,
|
||||
items = items,
|
||||
@@ -346,6 +397,27 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
|
||||
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
|
||||
return
|
||||
}
|
||||
if (event.root.type == EventType.STATE_ROOM_MEMBER
|
||||
&& event.root.stateKey == session.myUserId) {
|
||||
val content = event.root.content.toModel<RoomMemberContent>()
|
||||
if (content?.membership == Membership.INVITE) {
|
||||
hasReachedInvite = true
|
||||
} else if (content?.membership == Membership.JOIN) {
|
||||
val prevContent = event.root.resolvedPrevContent().toModel<RoomMemberContent>()
|
||||
if (prevContent?.membership?.isActive() == false) {
|
||||
hasReachedInvite = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextEvent?.root?.getClearType() == EventType.ENCRYPTED) {
|
||||
hasUTD = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
@@ -355,9 +427,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return shouldAdd
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
|
@@ -190,7 +190,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent)
|
||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary())
|
||||
}
|
||||
else -> null
|
||||
} ?: ""
|
||||
|
@@ -25,6 +25,8 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
@@ -36,7 +38,8 @@ class EncryptionItemFactory @Inject constructor(
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val informationDataFactory: MessageInformationDataFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider) {
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val session: Session) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
@@ -51,7 +54,13 @@ class EncryptionItemFactory @Inject constructor(
|
||||
val shield: StatusTileTimelineItem.ShieldUIState
|
||||
if (isSafeAlgorithm) {
|
||||
title = stringProvider.getString(R.string.encryption_enabled)
|
||||
description = stringProvider.getString(R.string.encryption_enabled_tile_description)
|
||||
description = stringProvider.getString(
|
||||
if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) {
|
||||
R.string.direct_room_encryption_enabled_tile_description
|
||||
} else {
|
||||
R.string.encryption_enabled_tile_description
|
||||
}
|
||||
)
|
||||
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
||||
} else {
|
||||
title = stringProvider.getString(R.string.encryption_not_enabled)
|
||||
|
@@ -22,6 +22,7 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
||||
@@ -30,22 +31,19 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedUTDItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedUTDItem_
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
private val roomSummaryHolder: RoomSummaryHolder) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
@@ -63,10 +61,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
callback: TimelineEventController.Callback?,
|
||||
requestModelBuild: () -> Unit)
|
||||
: BasedMergedItem<*>? {
|
||||
return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) {
|
||||
Timber.v("## MERGE: Candidate for merge, top event ${event.eventId}")
|
||||
buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, /*requestModelBuild,*/ callback)
|
||||
} else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
|
||||
// It's the first item before room.create
|
||||
// Collapse all room configuration events
|
||||
@@ -78,6 +73,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDirectRoom() = roomSummaryHolder.roomSummary?.isDirect.orFalse()
|
||||
|
||||
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
event: TimelineEvent,
|
||||
@@ -100,7 +97,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: ""
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom()
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
@@ -138,82 +136,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
}
|
||||
}
|
||||
|
||||
// Event should be UTD
|
||||
// Next event should not
|
||||
private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
|
||||
if (!vectorPreferences.mergeUTDinTimeline()) return false
|
||||
// if event is not UTD return false
|
||||
if (!isEventUTD(event)) return false
|
||||
// At this point event cannot be decrypted
|
||||
// Let's check if older event is not UTD
|
||||
return nextEvent == null || !isEventUTD(event)
|
||||
}
|
||||
|
||||
private fun isEventUTD(event: TimelineEvent): Boolean {
|
||||
return event.root.getClearType() == EventType.ENCRYPTED && !event.root.isRedacted()
|
||||
}
|
||||
|
||||
private fun buildUTDMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
event: TimelineEvent,
|
||||
eventIdToHighlight: String?,
|
||||
// requestModelBuild: () -> Unit,
|
||||
callback: TimelineEventController.Callback?): MergedUTDItem_? {
|
||||
Timber.v("## MERGE: buildUTDMergedSummary from position $currentPosition")
|
||||
var prevEvent = items.prevOrNull(currentPosition)
|
||||
var tmpPos = currentPosition - 1
|
||||
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
|
||||
|
||||
while (prevEvent != null && isEventUTD(prevEvent)) {
|
||||
mergedEvents.add(prevEvent)
|
||||
tmpPos--
|
||||
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
|
||||
}
|
||||
|
||||
Timber.v("## MERGE: buildUTDMergedSummary merge group size ${mergedEvents.size}")
|
||||
if (mergedEvents.size < 3) return null
|
||||
|
||||
var highlighted = false
|
||||
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||
mergedEvents.reversed()
|
||||
.forEach { mergedEvent ->
|
||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||
highlighted = true
|
||||
}
|
||||
val senderAvatar = mergedEvent.senderInfo.avatarUrl
|
||||
val senderName = mergedEvent.senderInfo.disambiguatedDisplayName
|
||||
val data = BasedMergedItem.Data(
|
||||
userId = mergedEvent.root.senderId ?: "",
|
||||
avatarUrl = senderAvatar,
|
||||
memberName = senderName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: ""
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
val mergedEventIds = mergedEvents.map { it.localId }
|
||||
|
||||
collapsedEventIds.addAll(mergedEventIds)
|
||||
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
|
||||
val attributes = MergedUTDItem.Attributes(
|
||||
isCollapsed = true,
|
||||
mergeData = mergedData,
|
||||
avatarRenderer = avatarRenderer,
|
||||
onCollapsedStateChanged = {}
|
||||
)
|
||||
return MergedUTDItem_()
|
||||
.id(mergeId)
|
||||
.big(mergedEventIds.size > 5)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(highlighted)
|
||||
.attributes(attributes)
|
||||
.also {
|
||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
event: TimelineEvent,
|
||||
@@ -247,7 +169,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: ""
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom()
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
|
@@ -38,6 +38,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
@@ -101,6 +102,7 @@ class MessageItemFactory @Inject constructor(
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
@@ -130,7 +132,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||
) {
|
||||
// This is an edit event, we should display it when debugging as a notice event
|
||||
return noticeItemFactory.create(event, highlight, callback)
|
||||
return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
}
|
||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||
|
||||
@@ -146,7 +148,7 @@ class MessageItemFactory @Inject constructor(
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -34,8 +35,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
roomSummary: RoomSummary?,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val formattedText = eventFormatter.format(event, roomSummary) ?: return null
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
|
@@ -21,6 +21,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@@ -32,6 +33,7 @@ import javax.inject.Inject
|
||||
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val session: Session,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val noticeItemFactory: NoticeItemFactory) {
|
||||
|
||||
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
@@ -52,7 +54,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
|
||||
|
||||
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, false, callback)
|
||||
noticeItemFactory.create(event, false, roomSummaryHolder.roomSummary, callback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import im.vector.app.core.epoxy.EmptyItem_
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
@@ -31,6 +32,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val encryptionItemFactory: EncryptionItemFactory,
|
||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
||||
@@ -63,10 +65,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
EventType.REACTION,
|
||||
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> {
|
||||
encryptionItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
// State room create
|
||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||
// Crypto
|
||||
@@ -87,7 +87,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
// TODO These are not filtered out by timeline when encrypted
|
||||
// For now manually ignore
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, highlight, callback)
|
||||
noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@@ -50,6 +51,7 @@ class VerificationItemFactory @Inject constructor(
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session
|
||||
) {
|
||||
@@ -151,7 +153,7 @@ class VerificationItemFactory @Inject constructor(
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import im.vector.app.core.resources.StringProvider
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
@@ -40,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor(
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
) {
|
||||
|
||||
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
|
||||
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean, roomSummary: RoomSummary?): CharSequence {
|
||||
if (timelineEvent.root.isRedacted()) {
|
||||
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class DisplayableEventFormatter @Inject constructor(
|
||||
}
|
||||
else -> {
|
||||
return span {
|
||||
text = noticeEventFormatter.format(timelineEvent) ?: ""
|
||||
text = noticeEventFormatter.format(timelineEvent, roomSummary) ?: ""
|
||||
textStyle = "italic"
|
||||
}
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
@@ -56,23 +57,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
|
||||
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
|
||||
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
|
||||
|
||||
fun format(timelineEvent: TimelineEvent, rs: RoomSummary?): CharSequence? {
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root)
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
|
||||
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_WIDGET,
|
||||
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
@@ -151,19 +155,19 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
fun format(event: Event, senderName: String?): CharSequence? {
|
||||
fun format(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
return when (val type = event.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName)
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
|
||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
@@ -175,14 +179,14 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
return "{ \"type\": ${event.getClearType()} }"
|
||||
}
|
||||
|
||||
private fun formatRoomCreateEvent(event: Event): CharSequence? {
|
||||
private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? {
|
||||
return event.getClearContent().toModel<RoomCreateContent>()
|
||||
?.takeIf { it.creator.isNullOrBlank().not() }
|
||||
?.let {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_created_by_you)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_created, it.creator)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,11 +208,11 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? {
|
||||
private fun formatRoomTombstoneEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
return if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_update_by_you)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_update, senderName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,18 +250,20 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
||||
|
||||
val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility)
|
||||
return if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_made_future_room_visibility_by_you, formattedVisibility)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
|
||||
formattedVisibility)
|
||||
} else {
|
||||
sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
|
||||
senderName, formattedVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?): CharSequence? {
|
||||
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
|
||||
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
|
||||
|
||||
@@ -265,17 +271,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
prevContent != null -> {
|
||||
// Revoke case
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_third_party_revoked_invite_by_you, prevContent.displayName)
|
||||
sp.getString(
|
||||
if (rs.isDm()) {
|
||||
R.string.notice_direct_room_third_party_revoked_invite_by_you
|
||||
} else {
|
||||
R.string.notice_room_third_party_revoked_invite_by_you
|
||||
},
|
||||
prevContent.displayName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
|
||||
senderName, prevContent.displayName)
|
||||
}
|
||||
}
|
||||
content != null -> {
|
||||
// Invitation case
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_third_party_invite_by_you, content.displayName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
|
||||
content.displayName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_third_party_invite, senderName, content.displayName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
|
||||
senderName, content.displayName)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
@@ -323,13 +338,13 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
|
||||
private fun formatRoomMemberEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
|
||||
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
|
||||
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
|
||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||
|| eventContent?.membership == Membership.LEAVE
|
||||
return if (isMembershipEvent) {
|
||||
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
|
||||
buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs)
|
||||
} else {
|
||||
buildProfileNotice(event, senderName, eventContent, prevEventContent)
|
||||
}
|
||||
@@ -387,20 +402,26 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?): String? {
|
||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
|
||||
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
|
||||
return when (eventContent?.guestAccess) {
|
||||
GuestAccess.CanJoin ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_guest_access_can_join_by_you)
|
||||
sp.getString(
|
||||
if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
|
||||
)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_guest_access_can_join, senderName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
|
||||
senderName)
|
||||
}
|
||||
GuestAccess.Forbidden ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_room_guest_access_forbidden_by_you)
|
||||
sp.getString(
|
||||
if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
|
||||
)
|
||||
} else {
|
||||
sp.getString(R.string.notice_room_guest_access_forbidden, senderName)
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
|
||||
senderName)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -476,7 +497,11 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
return displayText.toString()
|
||||
}
|
||||
|
||||
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? {
|
||||
private fun buildMembershipNotice(event: Event,
|
||||
senderName: String?,
|
||||
eventContent: RoomMemberContent?,
|
||||
prevEventContent: RoomMemberContent?,
|
||||
rs: RoomSummary?): String? {
|
||||
val senderDisplayName = senderName ?: event.senderId ?: ""
|
||||
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
|
||||
return when (eventContent?.membership) {
|
||||
@@ -524,14 +549,21 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
Membership.JOIN ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
eventContent.safeReason?.let { reason ->
|
||||
sp.getString(R.string.notice_room_join_with_reason_by_you, reason)
|
||||
} ?: sp.getString(R.string.notice_room_join_by_you)
|
||||
} else {
|
||||
eventContent.safeReason?.let { reason ->
|
||||
sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason)
|
||||
} ?: sp.getString(R.string.notice_room_join, senderDisplayName)
|
||||
eventContent.safeReason?.let { reason ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
|
||||
reason)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
|
||||
senderDisplayName, reason)
|
||||
}
|
||||
} ?: run {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join else R.string.notice_room_join,
|
||||
senderDisplayName)
|
||||
}
|
||||
}
|
||||
Membership.LEAVE ->
|
||||
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
|
||||
@@ -548,14 +580,27 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
} ?: sp.getString(R.string.notice_room_reject, senderDisplayName)
|
||||
}
|
||||
else ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
eventContent.safeReason?.let { reason ->
|
||||
sp.getString(R.string.notice_room_leave_with_reason_by_you, reason)
|
||||
} ?: sp.getString(R.string.notice_room_leave_by_you)
|
||||
} else {
|
||||
eventContent.safeReason?.let { reason ->
|
||||
sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason)
|
||||
} ?: sp.getString(R.string.notice_room_leave, senderDisplayName)
|
||||
eventContent.safeReason?.let { reason ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(
|
||||
if (rs.isDm()) {
|
||||
R.string.notice_direct_room_leave_with_reason_by_you
|
||||
} else {
|
||||
R.string.notice_room_leave_with_reason_by_you
|
||||
},
|
||||
reason
|
||||
)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
|
||||
senderDisplayName, reason)
|
||||
}
|
||||
} ?: run {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave else R.string.notice_room_leave,
|
||||
senderDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -618,14 +663,15 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? {
|
||||
private fun formatJoinRulesEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
|
||||
return when (content.joinRules) {
|
||||
RoomJoinRules.INVITE ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.room_join_rules_invite_by_you)
|
||||
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.room_join_rules_invite, senderName)
|
||||
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
|
||||
senderName)
|
||||
}
|
||||
RoomJoinRules.PUBLIC ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
|
@@ -62,7 +62,8 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
||||
val eventId: String,
|
||||
val userId: String,
|
||||
val memberName: String,
|
||||
val avatarUrl: String?
|
||||
val avatarUrl: String?,
|
||||
val isDirectRoom: Boolean
|
||||
)
|
||||
|
||||
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
|
||||
|
@@ -48,9 +48,17 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
|
||||
val createdFromCurrentUser = data?.userId == attributes.currentUserId
|
||||
val summary = if (createdFromCurrentUser) {
|
||||
holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
|
||||
if (data?.isDirectRoom == true) {
|
||||
holder.expandView.resources.getString(R.string.direct_room_created_summary_item_by_you)
|
||||
} else {
|
||||
holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
|
||||
}
|
||||
} else {
|
||||
holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
|
||||
if (data?.isDirectRoom == true) {
|
||||
holder.expandView.resources.getString(R.string.direct_room_created_summary_item, data.memberName)
|
||||
} else {
|
||||
holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
|
||||
}
|
||||
}
|
||||
holder.summaryView.text = summary
|
||||
holder.summaryView.visibility = View.VISIBLE
|
||||
@@ -69,7 +77,11 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||
}
|
||||
if (attributes.isEncryptionAlgorithmSecure) {
|
||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
|
||||
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
|
||||
} else {
|
||||
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
}
|
||||
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
|
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedUTDItem : BasedMergedItem<MergedUTDItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
override lateinit var attributes: Attributes
|
||||
|
||||
@EpoxyAttribute
|
||||
var big: Boolean? = false
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.mergedTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
this.marginEnd = leftGuideline
|
||||
if (big == true) {
|
||||
this.height = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
800f,
|
||||
holder.view.context.resources.displayMetrics
|
||||
).toInt()
|
||||
} else {
|
||||
this.height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
// if (attributes.isCollapsed) {
|
||||
// // Take the oldest data
|
||||
// val data = distinctMergeData.lastOrNull()
|
||||
//
|
||||
// val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
|
||||
// data?.memberName ?: data?.userId ?: "")
|
||||
// holder.summaryView.text = summary
|
||||
// holder.summaryView.visibility = View.VISIBLE
|
||||
// holder.avatarView.visibility = View.VISIBLE
|
||||
// if (data != null) {
|
||||
// holder.avatarView.visibility = View.VISIBLE
|
||||
// attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||
// } else {
|
||||
// holder.avatarView.visibility = View.GONE
|
||||
// }
|
||||
//
|
||||
// if (attributes.hasEncryptionEvent) {
|
||||
// holder.encryptionTile.isVisible = true
|
||||
// holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
// this.marginEnd = leftGuideline
|
||||
// }
|
||||
// if (attributes.isEncryptionAlgorithmSecure) {
|
||||
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
// holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
// null, null, null
|
||||
// )
|
||||
// } else {
|
||||
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
||||
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||
// null, null, null
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// holder.encryptionTile.isVisible = false
|
||||
// }
|
||||
// } else {
|
||||
// holder.avatarView.visibility = View.INVISIBLE
|
||||
// holder.summaryView.visibility = View.GONE
|
||||
// holder.encryptionTile.isGone = true
|
||||
// }
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||
// val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
// val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val mergedTile by bind<ViewGroup>(R.id.mergedUTDTile)
|
||||
//
|
||||
// val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
|
||||
// val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentMergedUTDStub
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
override val isCollapsed: Boolean,
|
||||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
) : BasedMergedItem.Attributes
|
||||
}
|
@@ -86,7 +86,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
||||
var latestEventTime: CharSequence = ""
|
||||
val latestEvent = roomSummary.latestPreviewableEvent
|
||||
if (latestEvent != null) {
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not(), roomSummary)
|
||||
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
}
|
||||
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
|
||||
|
@@ -146,8 +146,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
|
||||
LoginServerSelectionFragment::class.java,
|
||||
option = { ft ->
|
||||
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
})
|
||||
|
@@ -33,6 +33,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequest
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.ui.model.Size
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.isLocalFile
|
||||
@@ -206,12 +207,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
||||
}
|
||||
|
||||
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
|
||||
return if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
glideRequests.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
@@ -223,15 +226,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
// Fallback to base url
|
||||
?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
glideRequests
|
||||
.load(resolvedUrl)
|
||||
.apply {
|
||||
if (mode == Mode.THUMBNAIL) {
|
||||
error(
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolveUrl(data))
|
||||
glideRequests.load(resolveUrl(data))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.app.features.debug.DebugMenuActivity
|
||||
@@ -153,7 +154,10 @@ class DefaultNavigator @Inject constructor(
|
||||
|
||||
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
|
||||
if (context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
|
||||
BootstrapBottomSheet.show(
|
||||
context.supportFragmentManager,
|
||||
if (initCrossSigningOnly) SetupMode.CROSS_SIGNING_ONLY else SetupMode.NORMAL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,13 +230,19 @@ class DefaultNavigator @Inject constructor(
|
||||
// if cross signing is enabled we should propose full 4S
|
||||
sessionHolder.getSafeActiveSession()?.let { session ->
|
||||
if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
|
||||
} else {
|
||||
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun open4SSetup(context: Context, setupMode: SetupMode) {
|
||||
if (context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, setupMode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openKeysBackupManager(context: Context) {
|
||||
context.startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.util.Pair
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
|
||||
import im.vector.app.features.media.AttachmentData
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
@@ -71,6 +72,8 @@ interface Navigator {
|
||||
|
||||
fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
|
||||
|
||||
fun open4SSetup(context: Context, setupMode: SetupMode)
|
||||
|
||||
fun openKeysBackupManager(context: Context)
|
||||
|
||||
fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false)
|
||||
|
@@ -91,7 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
||||
if (room == null) {
|
||||
Timber.e("## Unable to resolve room for eventId [$event]")
|
||||
// Ok room is not known in store, but we can still display something
|
||||
val body = displayableEventFormatter.format(event, false)
|
||||
val body = displayableEventFormatter.format(event, false, null)
|
||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
@@ -124,7 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
||||
}
|
||||
}
|
||||
|
||||
val body = displayableEventFormatter.format(event, false).toString()
|
||||
val body = displayableEventFormatter.format(event, false, room.roomSummary()).toString()
|
||||
val roomName = room.roomSummary()?.displayName ?: ""
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
@@ -165,7 +165,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
||||
val roomId = event.roomId ?: return null
|
||||
val dName = event.senderId?.let { session.getUser(it)?.displayName }
|
||||
if (Membership.INVITE == content.membership) {
|
||||
val body = noticeEventFormatter.format(event, dName)
|
||||
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
|
||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||
return InviteNotifiableEvent(
|
||||
session.myUserId,
|
||||
|
@@ -81,6 +81,11 @@ class PinLocker @Inject constructor(
|
||||
computeState()
|
||||
}
|
||||
|
||||
fun screenIsOff() {
|
||||
shouldBeLocked = true
|
||||
computeState()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
|
||||
|
@@ -27,6 +27,7 @@ import com.tapadoo.alerter.OnHideAlertListener
|
||||
import dagger.Lazy
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.isAnimationDisabled
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
@@ -172,6 +173,8 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
|
||||
clearLightStatusBar()
|
||||
|
||||
val noAnimation = !animate || isAnimationDisabled(activity)
|
||||
|
||||
alert.weakCurrentActivity = WeakReference(activity)
|
||||
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
|
||||
else Alerter.create(activity)
|
||||
@@ -187,7 +190,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
}
|
||||
}
|
||||
.apply {
|
||||
if (!animate) {
|
||||
if (noAnimation) {
|
||||
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
||||
}
|
||||
|
||||
@@ -237,6 +240,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
|
||||
}
|
||||
}
|
||||
.enableIconPulse(!noAnimation)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.app.features.roommemberprofile.devices
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.KeyEvent
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
@@ -29,6 +30,7 @@ import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -104,10 +106,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val userId: String,
|
||||
val allowDeviceAction: Boolean
|
||||
) : Parcelable
|
||||
|
||||
companion object {
|
||||
fun newInstance(userId: String): DeviceListBottomSheet {
|
||||
fun newInstance(userId: String, allowDeviceAction: Boolean = true): DeviceListBottomSheet {
|
||||
val args = Bundle()
|
||||
args.putString(MvRx.KEY_ARG, userId)
|
||||
args.putParcelable(MvRx.KEY_ARG, Args(userId, allowDeviceAction))
|
||||
return DeviceListBottomSheet().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.airbnb.mvrx.args
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.di.HasScreenInjector
|
||||
@@ -44,24 +45,24 @@ data class DeviceListViewState(
|
||||
) : MvRxState
|
||||
|
||||
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
|
||||
@Assisted private val userId: String,
|
||||
@Assisted private val args: DeviceListBottomSheet.Args,
|
||||
private val session: Session)
|
||||
: VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
|
||||
fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel
|
||||
}
|
||||
|
||||
init {
|
||||
session.rx().liveUserCryptoDevices(userId)
|
||||
session.rx().liveUserCryptoDevices(args.userId)
|
||||
.execute {
|
||||
copy(cryptoDevices = it).also {
|
||||
refreshSelectedId()
|
||||
}
|
||||
}
|
||||
|
||||
session.rx().liveCrossSigningInfo(userId)
|
||||
session.rx().liveCrossSigningInfo(args.userId)
|
||||
.execute {
|
||||
copy(memberCrossSigningKey = it.invoke()?.getOrNull())
|
||||
}
|
||||
@@ -88,6 +89,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
}
|
||||
|
||||
private fun selectDevice(action: DeviceListAction.SelectDevice) {
|
||||
if (!args.allowDeviceAction) return
|
||||
setState {
|
||||
copy(selectedDevice = action.device)
|
||||
}
|
||||
@@ -100,8 +102,9 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
}
|
||||
|
||||
private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
|
||||
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
|
||||
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
|
||||
if (!args.allowDeviceAction) return
|
||||
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, args.userId, action.deviceId, null)?.let { txID ->
|
||||
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(args.userId, txID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,12 +112,12 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? {
|
||||
val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val userId = viewModelContext.args<String>()
|
||||
return fragment.viewModelFactory.create(state, userId)
|
||||
val args = viewModelContext.args<DeviceListBottomSheet.Args>()
|
||||
return fragment.viewModelFactory.create(state, args)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
|
||||
val userId = viewModelContext.args<String>()
|
||||
val userId = viewModelContext.args<DeviceListBottomSheet.Args>().userId
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
return session.getUser(userId)?.toMatrixItem()?.let {
|
||||
DeviceListViewState(
|
||||
|
@@ -18,7 +18,6 @@
|
||||
package im.vector.app.features.roomprofile
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.profiles.buildProfileAction
|
||||
import im.vector.app.core.epoxy.profiles.buildProfileSection
|
||||
@@ -26,6 +25,7 @@ import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomProfileController @Inject constructor(
|
||||
@@ -57,9 +57,9 @@ class RoomProfileController @Inject constructor(
|
||||
// Security
|
||||
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
|
||||
val learnMoreSubtitle = if (roomSummary.isEncrypted) {
|
||||
R.string.room_profile_encrypted_subtitle
|
||||
if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle
|
||||
} else {
|
||||
R.string.room_profile_not_encrypted_subtitle
|
||||
if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle else R.string.room_profile_not_encrypted_subtitle
|
||||
}
|
||||
genericFooterItem {
|
||||
id("e2e info")
|
||||
@@ -71,7 +71,11 @@ class RoomProfileController @Inject constructor(
|
||||
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
|
||||
buildProfileAction(
|
||||
id = "settings",
|
||||
title = stringProvider.getString(R.string.room_profile_section_more_settings),
|
||||
title = stringProvider.getString(if (roomSummary.isDirect) {
|
||||
R.string.direct_room_profile_section_more_settings
|
||||
} else {
|
||||
R.string.room_profile_section_more_settings
|
||||
}),
|
||||
dividerColor = dividerColor,
|
||||
icon = R.drawable.ic_room_profile_settings,
|
||||
action = { callback?.onSettingsClicked() }
|
||||
@@ -112,7 +116,11 @@ class RoomProfileController @Inject constructor(
|
||||
)
|
||||
buildProfileAction(
|
||||
id = "leave",
|
||||
title = stringProvider.getString(R.string.room_profile_section_more_leave),
|
||||
title = stringProvider.getString(if (roomSummary.isDirect) {
|
||||
R.string.direct_room_profile_section_more_leave
|
||||
} else {
|
||||
R.string.room_profile_section_more_leave
|
||||
}),
|
||||
dividerColor = dividerColor,
|
||||
divider = false,
|
||||
destructive = true,
|
||||
|
@@ -153,7 +153,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
|
||||
|
||||
// SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS
|
||||
private const val SETTINGS_LABS_MERGE_E2E_ERRORS = "SETTINGS_LABS_MERGE_E2E_ERRORS"
|
||||
private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
|
||||
const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
|
||||
|
||||
// analytics
|
||||
@@ -285,8 +285,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
|
||||
}
|
||||
|
||||
fun mergeUTDinTimeline(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_MERGE_E2E_ERRORS, false)
|
||||
fun labShowCompleteHistoryInEncryptedRoom(): Boolean {
|
||||
return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM, false)
|
||||
}
|
||||
|
||||
fun labAllowedExtendedLogging(): Boolean {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user