mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Merge branch 'develop' into feature/performance_tracking
This commit is contained in:
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
|
38
CHANGES.md
38
CHANGES.md
@@ -1,9 +1,36 @@
|
||||
Changes in Element 1.0.8 (2020-XX-XX)
|
||||
Changes in Element 1.0.9 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
||||
|
||||
Improvements 🙌:
|
||||
- Wording differentiation for direct rooms (#2176)
|
||||
- PIN code: request PIN code if phone has been locked
|
||||
- Small optimisation of scrolling experience in timeline (#2114)
|
||||
- Allow user to reset cross signing if he has no way to recover (#2052)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Improve support for image/audio/video/file selection with intent changes (#1376)
|
||||
- Fix Splash layout on small screens
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
- Use Update Gradle Wrapper Action
|
||||
- Updates Gradle Wrapper from 5.6.4 to 6.6.1. (#2193)
|
||||
|
||||
Other changes:
|
||||
- Added registration/verification automated UI tests
|
||||
- Create a script to help getting public information form any homeserver
|
||||
|
||||
Changes in Element 1.0.8 (2020-09-25)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Add "show password" in import Megolm keys dialog
|
||||
- Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112)
|
||||
@@ -15,22 +42,17 @@ Improvements 🙌:
|
||||
- PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Long message cannot be sent/takes infinite time & blocks other messages #1397
|
||||
- Long message cannot be sent/takes infinite time & blocks other messages (#1397)
|
||||
- Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes)
|
||||
- User Verification in DM not working
|
||||
- Manual import of Megolm keys does back up the imported keys
|
||||
- Auto scrolling to the latest message when sending (#2094)
|
||||
- Fix incorrect permission check when creating widgets (#2137)
|
||||
- Pin code: user has to enter pin code twice (#2005)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- Rename `tryThis` to `tryOrNull`
|
||||
|
||||
Build 🧱:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- Add an advanced action to reset an account data entry
|
||||
|
||||
|
107
docs/ui-tests.md
Normal file
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
|
||||
|
@@ -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()
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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===79
|
||||
enum class===81
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
69
tools/hs_diag.py
Executable file
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")
|
||||
@@ -329,6 +347,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
|
||||
|
||||
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
||||
implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version"
|
||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
|
||||
implementation 'com.airbnb.android:mvrx:1.3.0'
|
||||
@@ -424,19 +443,21 @@ dependencies {
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
// Activate when you want to check for leaks, from time to time.
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "androidx.test:core:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:runner:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:rules:$androidxTest_version"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
|
||||
androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
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() {
|
||||
|
@@ -77,4 +77,6 @@ data class RoomDetailViewState(
|
||||
// Also highlight the target event, if any
|
||||
highlightedEventId = args.eventId
|
||||
)
|
||||
|
||||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||
}
|
||||
|
@@ -51,18 +51,28 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||
private val session: Session,
|
||||
@TimelineEventControllerHandler
|
||||
private val backgroundHandler: Handler
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
@@ -114,9 +124,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var hasReachedInvite: Boolean = false
|
||||
private var hasUTD: Boolean = false
|
||||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var previousModelsSize = 0
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
@@ -193,6 +206,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
models.add(position, readMarker)
|
||||
}
|
||||
}
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
|
||||
models.add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
models.add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
@@ -243,7 +279,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return
|
||||
}
|
||||
// Avoid displaying two loaders if there is no elements between them
|
||||
val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty()
|
||||
// We can hide the loader but still add the item to controller so it can trigger backwards pagination
|
||||
@@ -303,6 +341,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
|
||||
hasUTD = false
|
||||
hasReachedInvite = false
|
||||
|
||||
if (modelCache.isEmpty()) {
|
||||
return
|
||||
}
|
||||
@@ -318,13 +359,21 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextOrNull(currentPosition)
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
||||
}
|
||||
updateUTDStates(event, nextEvent)
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val addDaySeparator = if (hasReachedInvite && hasUTD) {
|
||||
true
|
||||
} else {
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
date.toLocalDate() != nextDate?.toLocalDate()
|
||||
}
|
||||
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
|
||||
nextEvent = nextEvent,
|
||||
items = items,
|
||||
@@ -348,6 +397,27 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
|
||||
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
|
||||
return
|
||||
}
|
||||
if (event.root.type == EventType.STATE_ROOM_MEMBER
|
||||
&& event.root.stateKey == session.myUserId) {
|
||||
val content = event.root.content.toModel<RoomMemberContent>()
|
||||
if (content?.membership == Membership.INVITE) {
|
||||
hasReachedInvite = true
|
||||
} else if (content?.membership == Membership.JOIN) {
|
||||
val prevContent = event.root.resolvedPrevContent().toModel<RoomMemberContent>()
|
||||
if (prevContent?.membership?.isActive() == false) {
|
||||
hasReachedInvite = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextEvent?.root?.getClearType() == EventType.ENCRYPTED) {
|
||||
hasUTD = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
@@ -357,9 +427,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return shouldAdd
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.app.features.raw.wellknown
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
object ElementWellKnownMapper {
|
||||
@@ -24,6 +25,6 @@ object ElementWellKnownMapper {
|
||||
val adapter: JsonAdapter<ElementWellKnown> = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java)
|
||||
|
||||
fun from(value: String): ElementWellKnown? {
|
||||
return adapter.fromJson(value)
|
||||
return tryOrNull("Unable to parse well-known data") { adapter.fromJson(value) }
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -53,11 +53,12 @@ import im.vector.app.features.crypto.keys.KeysExporter
|
||||
import im.vector.app.features.crypto.keys.KeysImporter
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.pin.PinCodeStore
|
||||
import im.vector.app.features.pin.PinMode
|
||||
import im.vector.app.features.raw.wellknown.ElementWellKnownMapper
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
@@ -153,14 +154,13 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
disposables.add(it)
|
||||
}
|
||||
|
||||
vectorActivity.getVectorComponent()
|
||||
.rawService()
|
||||
.getWellknown(session.myUserId, object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
|
||||
ElementWellKnownMapper.from(data)?.isE2EByDefault() == false
|
||||
}
|
||||
})
|
||||
lifecycleScope.launchWhenResumed {
|
||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
|
||||
vectorActivity.getVectorComponent()
|
||||
.rawService()
|
||||
.getElementWellknown(session.myUserId)
|
||||
?.isE2EByDefault() == false
|
||||
}
|
||||
}
|
||||
|
||||
private val secureBackupCategory by lazy {
|
||||
@@ -194,7 +194,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
secureBackupCategory.isVisible = true
|
||||
secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
|
||||
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@@ -213,7 +213,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
secureBackupCategory.isVisible = true
|
||||
secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
|
||||
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.PASSPHRASE_RESET)
|
||||
true
|
||||
}
|
||||
} else if (!info.megolmSecretKnown) {
|
||||
|
@@ -44,6 +44,7 @@ import im.vector.app.core.extensions.queryExportKeys
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -121,7 +122,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
setupRecoveryButton.action = {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
|
||||
}
|
||||
|
||||
exitAnywayButton.action = {
|
||||
|
@@ -3,144 +3,187 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?riotx_background">
|
||||
android:background="?riotx_background"
|
||||
android:paddingStart="36dp"
|
||||
android:paddingTop="@dimen/layout_vertical_margin"
|
||||
android:paddingEnd="36dp"
|
||||
android:paddingBottom="@dimen/layout_vertical_margin">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<!-- Strategy: 5 Spaces are used to spread the remaining space, using weight -->
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashLogoContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside"
|
||||
app:layout_constraintVertical_weight="4" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loginSplashLogoContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
style="@style/LoginContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashLogo"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/element_logo_green"
|
||||
android:transitionName="loginLogoTransition" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashLogo"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/element_logo_green"
|
||||
android:transitionName="loginLogoTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/logoType"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
<ImageView
|
||||
android:id="@+id/logoType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@drawable/element_logotype"
|
||||
android:tint="?colorAccent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logoType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@drawable/element_logotype"
|
||||
android:tint="?colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginSplashLogo" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/login_splash_title"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
|
||||
android:transitionName="loginTitleTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/logoType" />
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashLogoContainer"
|
||||
app:layout_constraintVertical_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_message_circle"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/login_splash_title"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
|
||||
android:transitionName="loginTitleTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="96dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text1"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle" />
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashContent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle"
|
||||
app:layout_constraintVertical_weight="2" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_lock"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loginSplashContent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace3">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text2"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_message_circle"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_sliders"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text1"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text3"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_lock"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginSplashSubmit"
|
||||
style="@style/Style.Vector.Login.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:text="@string/login_splash_submit"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText3" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text2"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_sliders"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text3"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashContent"
|
||||
app:layout_constraintVertical_weight="2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginSplashSubmit"
|
||||
style="@style/Style.Vector.Login.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_splash_submit"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace4" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintVertical_weight="4" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@@ -15,11 +15,11 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:src="@drawable/ic_security_key_24dp"
|
||||
android:tint="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
|
||||
android:src="@drawable/ic_security_key_24dp" />
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_key"
|
||||
@@ -56,8 +56,8 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -97,16 +97,32 @@
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_key_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ssss_key_reset"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til"
|
||||
app:layout_goneMarginBottom="@dimen/layout_vertical_margin_big" />
|
||||
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/ssss_key_reset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_key_flow"
|
||||
app:leftIcon="@drawable/ic_alert_triangle"
|
||||
app:tint="@color/vector_error_color"
|
||||
app:titleTextColor="?riotx_text_secondary" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
@@ -109,16 +109,33 @@
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_passphrase_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ssss_passphrase_reset"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til"
|
||||
app:layout_goneMarginBottom="32dp" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/ssss_passphrase_reset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_flow"
|
||||
app:leftIcon="@drawable/ic_alert_triangle"
|
||||
app:tint="@color/vector_error_color"
|
||||
app:titleTextColor="?riotx_text_secondary" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
119
vector/src/main/res/layout/fragment_ssss_reset_all.xml
Normal file
119
vector/src/main/res/layout/fragment_ssss_reset_all.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ssss__root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reset_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:drawableStart="@drawable/ic_alert_triangle"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableTint="?riot_primary_text_color"
|
||||
android:text="@string/secure_backup_reset_all"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:tint="?riot_primary_text_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_all_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/secure_backup_reset_all_no_other_devices"
|
||||
android:textColor="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices"
|
||||
app:layout_constraintTop_toBottomOf="@id/reset_title" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_other_devices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:drawableStart="@drawable/ic_smartphone"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="17sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_all_description"
|
||||
tools:text="Show 2 devices you can verify with now"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_text3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/secure_backup_reset_if_you_reset_all"
|
||||
android:textColor="@color/riotx_destructive_accent"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:tint="?riot_primary_text_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_text4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/secure_backup_reset_no_history"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text3" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_reset_button_cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_reset_button_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/reset"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_passphrase_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:constraint_referenced_ids="ssss_reset_button_cancel, ssss_reset_button_reset"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text4" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@@ -52,6 +52,7 @@
|
||||
|
||||
<declare-styleable name="BottomSheetActionButton">
|
||||
<attr name="tint" format="color" />
|
||||
<attr name="titleTextColor" format="color" />
|
||||
<attr name="actionTitle" format="string" />
|
||||
<attr name="actionDescription" format="string" />
|
||||
<attr name="leftIcon" format="reference" />
|
||||
|
@@ -78,6 +78,7 @@
|
||||
<string name="play_video">Play</string>
|
||||
<string name="pause_video">Pause</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="reset">Reset</string>
|
||||
|
||||
|
||||
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
|
||||
@@ -89,7 +90,9 @@
|
||||
<string name="missing_permissions_error">"Due to missing permissions, this action is not possible.</string>
|
||||
<string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>
|
||||
<string name="no_permissions_to_start_conf_call">You do not have permission to start a conference call in this room</string>
|
||||
<string name="no_permissions_to_start_conf_call_in_direct_room">You do not have permission to start a conference call</string>
|
||||
<string name="no_permissions_to_start_webrtc_call">You do not have permission to start a call in this room</string>
|
||||
<string name="no_permissions_to_start_webrtc_call_in_direct_room">You do not have permission to start a call</string>
|
||||
<string name="conference_call_in_progress">A conference is already in progress!</string>
|
||||
<string name="video_meeting">Start video meeting</string>
|
||||
<string name="audio_meeting">Start audio meeting</string>
|
||||
@@ -1708,6 +1711,7 @@
|
||||
<string name="send_suggestion_failed">The suggestion failed to be sent (%s)</string>
|
||||
|
||||
<string name="settings_labs_show_hidden_events_in_timeline">Show hidden events in timeline</string>
|
||||
<string name="settings_labs_show_complete_history_in_encrypted_room">"Show complete history in encrypted rooms"</string>
|
||||
|
||||
<string name="bottom_action_people_x">Direct Messages</string>
|
||||
|
||||
@@ -1881,6 +1885,8 @@
|
||||
<string name="room_join_rules_public_by_you">You made the room public to whoever knows the link.</string>
|
||||
<string name="room_join_rules_invite">%1$s made the room invite only.</string>
|
||||
<string name="room_join_rules_invite_by_you">You made the room invite only.</string>
|
||||
<string name="direct_room_join_rules_invite">%1$s made this invite only.</string>
|
||||
<string name="direct_room_join_rules_invite_by_you">You made this invite only.</string>
|
||||
<string name="timeline_unread_messages">Unread messages</string>
|
||||
|
||||
<string name="login_splash_title">It\'s your conversation. Own it.</string>
|
||||
@@ -2113,12 +2119,15 @@
|
||||
<string name="verification_request_waiting_for">Waiting for %s…</string>
|
||||
<string name="verification_request_alert_description">For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.</string>
|
||||
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
|
||||
<string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string>
|
||||
<string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
|
||||
<string name="direct_room_profile_encrypted_subtitle">Messages here are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
|
||||
<string name="room_profile_section_security">Security</string>
|
||||
<string name="room_profile_section_security_learn_more">Learn more</string>
|
||||
<string name="room_profile_section_more">More</string>
|
||||
<string name="room_profile_section_admin">Admin Actions</string>
|
||||
<string name="room_profile_section_more_settings">Room settings</string>
|
||||
<string name="direct_room_profile_section_more_settings">Settings</string>
|
||||
<string name="room_profile_section_more_notifications">Notifications</string>
|
||||
<plurals name="room_profile_section_more_member_list">
|
||||
<item quantity="one">"One person"</item>
|
||||
@@ -2126,6 +2135,7 @@
|
||||
</plurals>
|
||||
<string name="room_profile_section_more_uploads">Uploads</string>
|
||||
<string name="room_profile_section_more_leave">Leave Room</string>
|
||||
<string name="direct_room_profile_section_more_leave">Leave</string>
|
||||
<string name="room_profile_leaving_room">"Leaving the room…"</string>
|
||||
|
||||
<string name="room_member_power_level_admins">Admins</string>
|
||||
@@ -2355,11 +2365,14 @@
|
||||
|
||||
<string name="encryption_enabled">Encryption enabled</string>
|
||||
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & verify users in their profile.</string>
|
||||
<string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string>
|
||||
<string name="encryption_not_enabled">Encryption not enabled</string>
|
||||
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
|
||||
|
||||
<string name="room_created_summary_item">%s created and configured the room.</string>
|
||||
<string name="room_created_summary_item_by_you">You created and configured the room.</string>
|
||||
<string name="direct_room_created_summary_item">%s joined.</string>
|
||||
<string name="direct_room_created_summary_item_by_you">You joined.</string>
|
||||
|
||||
<string name="qr_code_scanned_self_verif_notice">Almost there! Is the other device showing the same shield?</string>
|
||||
<string name="qr_code_scanned_verif_waiting_notice">Almost there! Waiting for confirmation…</string>
|
||||
@@ -2434,6 +2447,15 @@
|
||||
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
|
||||
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
|
||||
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
|
||||
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
|
||||
<string name="secure_backup_reset_all">Reset everything</string>
|
||||
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
|
||||
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
|
||||
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
|
||||
<plurals name="secure_backup_reset_devices_you_can_verify">
|
||||
<item quantity="one">Show the device you can verify with now</item>
|
||||
<item quantity="other">Show %d devices you can verify with now</item>
|
||||
</plurals>
|
||||
|
||||
<string name="unencrypted">Unencrypted</string>
|
||||
<string name="encrypted_unverified">Encrypted by an unverified device</string>
|
||||
|
@@ -1,13 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="LoginContainer">
|
||||
<item name="android:paddingTop">32dp</item>
|
||||
<item name="android:paddingBottom">32dp</item>
|
||||
<item name="android:paddingStart">36dp</item>
|
||||
<item name="android:paddingEnd">36dp</item>
|
||||
</style>
|
||||
|
||||
<item name="loginLogo" type="id" />
|
||||
<item name="loginFormScrollView" type="id" />
|
||||
<item name="loginFormContainer" type="id" />
|
||||
|
@@ -16,6 +16,12 @@
|
||||
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
|
||||
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
|
||||
android:key="SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
|
||||
android:title="@string/settings_labs_show_complete_history_in_encrypted_room" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
|
||||
|
@@ -39,13 +39,6 @@
|
||||
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
|
||||
android:title="@string/labs_swipe_to_reply_in_timeline" />
|
||||
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_MERGE_E2E_ERRORS"
|
||||
android:title="@string/labs_merge_e2e_in_timeline" />
|
||||
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
|
||||
|
Reference in New Issue
Block a user