diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000000000000000000000000000000000..681f41ae2aee4749eb4ddda94f8c6a76c825c825 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <codeStyleSettings language="XML"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + </indentOptions> + <arrangement> + <rules> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:android</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:id</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>ANDROID_ATTRIBUTE_ORDER</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>.*</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + </rules> + </arrangement> + </codeStyleSettings> + </code_scheme> +</component> \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb2873e7ed57bab6887aec5b9785a87c916facee --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RemoteRepositoriesConfiguration"> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="Maven Central repository" /> + <option name="url" value="https://repo1.maven.org/maven2" /> + </remote-repository> + <remote-repository> + <option name="id" value="jboss.community" /> + <option name="name" value="JBoss Community repository" /> + <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> + </remote-repository> + <remote-repository> + <option name="id" value="BintrayJCenter" /> + <option name="name" value="BintrayJCenter" /> + <option name="url" value="https://jcenter.bintray.com/" /> + </remote-repository> + <remote-repository> + <option name="id" value="maven" /> + <option name="name" value="maven" /> + <option name="url" value="https://jitpack.io" /> + </remote-repository> + <remote-repository> + <option name="id" value="Google" /> + <option name="name" value="Google" /> + <option name="url" value="https://dl.google.com/dl/android/maven2/" /> + </remote-repository> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..7bfef59df1ca77948220ecd36b0f19f148ab7914 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/build/classes" /> + </component> + <component name="ProjectType"> + <option name="id" value="Android" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000000000000000000000000000000000000..7f68460d8b38ac04e3a3224d7c79ef719b1991a9 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RunConfigurationProducerService"> + <option name="ignoredProducers"> + <set> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" /> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" /> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" /> + </set> + </option> + </component> +</project> \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000000000000000000000000000000000000..040b60d1a4e4fd80443f04f288b15547aac1e3e4 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,9 @@ +Please refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/master/CHANGES.md + +Changes in Matrix-SDK 0.0.1 (2020-08-14) +=================================================== + +This is the first release of the Matrix SDK. + +This first release has been created from the develop branch of Element Android ([at this commit](https://github.com/vector-im/element-android/commit/5a3894036cb34d00177603e69c5b15431212152d)). +Next releases will be done from master branch of Element Android and the version name will be the same between the SDK and Element Android diff --git a/README.md b/README.md index 4ae2d0a11caafb3a28e3097b7d9d8e2dd214204f..9e7074ee5ade78e3b932e268d603dc15601038ce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # matrix-android-sdk2 -Matrix SDK for Android, extracted from the Element Android application + +Matrix SDK for Android, extracted from the Element Android application. + +The SDK is still in beta, and replaces the [legacy Matrix Android SDK](https://github.com/matrix-org/matrix-android-sdk) provided by Matrix.org + +## About + +This repository contains the matrix-android-sdk extracted from the project [Element Android](https://github.com/vector-im/element-android) + +Please open any issue in the Element Android project [Create an issue](https://github.com/vector-im/element-android/issues/new/choose) + +## How to integrate the SDK in your application + +To integrate the SDK to your application, add the following gradle dependency to the build.gradle of your application module: + +> implementation 'com.github.matrix-org:matrix-android-sdk2:v0.0.1' + +You need to add Jitpack as a repository in your main build.gradle file. Please follow instructions here: https://jitpack.io/ + +## Migrate from legacy SDK + +Sadly there is no official documentation on how to migrate from the old SDK to the new one. Because the new SDK API is totally new, we guess that there is no easy way to handle a migration. + +We advice that new applications uses this new SDK. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..44384b9f7d376074b501b21e61d85e783ffdec45 --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.72' + repositories { + google() + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + // Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment + classpath 'com.android.tools.build:gradle:3.5.3' + classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + // For olm library. This has to be declared first, to ensure that Olm library is not downloaded from another repo + maven { + url 'https://jitpack.io' + content { + // Use this repo only for olm library + includeGroupByRegex "org\\.matrix\\.gitlab\\.matrix-org" + // And monarchy + includeGroupByRegex "com\\.github\\.Zhuinden" + } + } + google() + jcenter() + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + // Warnings are potential errors, so stop ignoring them + kotlinOptions.allWarningsAsErrors = true + } + +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000000000000000000000000000000000..99fd9d64fd27db2fd478ade850bd45130f1b43cb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + + +vector.debugPrivateData=false +vector.httpLogLevel=NONE + +# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above +#vector.debugPrivateData=true +#vector.httpLogLevel=BODY diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..4da2435f42fd8b0638d0f82a0b507f0f19e056e0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jul 02 12:33:07 CEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000000000000000000000000000000000..af6708ff229fda75da4f7cc4da4747217bac4d53 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +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 + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +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 + 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 + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((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" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..6d57edc706c93465988754383a2d7ff353d4e79f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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" + +@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 + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/matrix-sdk-android/.gitignore b/matrix-sdk-android/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/matrix-sdk-android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..a96e0690dc1868e744841715d8d26661a27761ee --- /dev/null +++ b/matrix-sdk-android/build.gradle @@ -0,0 +1,209 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'realm-android' +apply plugin: 'okreplay' + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "io.realm:realm-gradle-plugin:6.1.0" + } +} + +androidExtensions { + experimental = true +} + +android { + compileSdkVersion 29 + testOptions.unitTests.includeAndroidResources = true + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "0.0.1" + // Multidex is useful for tests + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + // 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' + + buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" + resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" + resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" + resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" + + defaultConfig { + consumerProguardFiles 'proguard-rules.pro' + } + } + + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } + + buildTypes { + debug { + // Set to true to log privacy or sensible data, such as token + buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") + // Set to BODY instead of NONE to enable logging + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level." + project.property("vector.httpLogLevel") + } + + release { + buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + } + } + + adbOptions { + installOptions "-g" + } + + lintOptions { + lintConfig file("lint.xml") + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + sourceSets { + androidTest { + java.srcDirs += "src/sharedTest/java" + } + test { + java.srcDirs += "src/sharedTest/java" + } + } +} + +static def gitRevision() { + def cmd = "git rev-parse --short=8 HEAD" + return cmd.execute().text.trim() +} + +static def gitRevisionUnixDate() { + def cmd = "git show -s --format=%ct HEAD^{commit}" + return cmd.execute().text.trim() +} + +static def gitRevisionDate() { + def cmd = "git show -s --format=%ci HEAD^{commit}" + return cmd.execute().text.trim() +} + +dependencies { + + def arrow_version = "0.8.2" + def moshi_version = '1.8.0' + def lifecycle_version = '2.2.0' + def arch_version = '2.1.0' + def coroutines_version = "1.3.2" + def markwon_version = '3.1.0' + def daggerVersion = '2.25.4' + def work_version = '2.3.3' + def retrofit_version = '2.6.2' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.core:core-ktx:1.3.0" + + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + + // Network + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + implementation "com.squareup.moshi:moshi-adapters:$moshi_version" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + + implementation "ru.noties.markwon:core:$markwon_version" + + // Image + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' + implementation 'id.zelory:compressor:3.0.0' + + // Database + implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' + kapt 'dk.ilios:realmfieldnameshelper:1.1.1' + + // Work + implementation "androidx.work:work-runtime-ktx:$work_version" + + // FP + implementation "io.arrow-kt:arrow-core:$arrow_version" + implementation "io.arrow-kt:arrow-instances-core:$arrow_version" + + // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm + implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2' + + // DI + implementation "com.google.dagger:dagger:$daggerVersion" + kapt "com.google.dagger:dagger-compiler:$daggerVersion" + compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0' + kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' + + // Logging + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' + + // Bus + implementation 'org.greenrobot:eventbus:3.1.1' + + // Phone number https://github.com/google/libphonenumber + implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' + + // Web RTC + // TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/ + implementation 'org.webrtc:google-webrtc:1.0.+' + + debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' + releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' + androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:4.3' + //testImplementation 'org.robolectric:shadows-support-v4:3.0' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + testImplementation 'io.mockk:mockk:1.9.2.kotlin12' + testImplementation 'org.amshove.kluent:kluent-android:1.44' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // Plant Timber tree for test + testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + + kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" + 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' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12' + androidTestImplementation "androidx.arch.core:core-testing:$arch_version" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // Plant Timber tree for test + androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + + androidTestUtil 'androidx.test:orchestrator:1.2.0' +} diff --git a/matrix-sdk-android/lint.xml b/matrix-sdk-android/lint.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e4078d7d979c7d2db009e6a81a6744afc67ba7f --- /dev/null +++ b/matrix-sdk-android/lint.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<lint> + <!-- Modify some severity --> + + <!-- Resource --> + <issue id="MissingTranslation" severity="warning" /> + <issue id="TypographyEllipsis" severity="error" /> + <issue id="ImpliedQuantity" severity="warning" /> + + <!-- UX --> + <issue id="ButtonOrder" severity="error" /> + + <!-- Layout --> + <issue id="UnknownIdInLayout" severity="error" /> + <issue id="StringFormatCount" severity="error" /> + <issue id="HardcodedText" severity="error" /> + <issue id="SpUsage" severity="error" /> + <issue id="ObsoleteLayoutParam" severity="error" /> + <issue id="InefficientWeight" severity="error" /> + <issue id="DisableBaselineAlignment" severity="error" /> + <issue id="ScrollViewSize" severity="error" /> + + <!-- RTL --> + <issue id="RtlEnabled" severity="error" /> + <issue id="RtlHardcoded" severity="error" /> + <issue id="RtlSymmetry" severity="error" /> + + <!-- Code --> + <issue id="SetTextI18n" severity="error" /> + <issue id="ViewConstructor" severity="error" /> + <issue id="UseValueOf" severity="error" /> + +</lint> diff --git a/matrix-sdk-android/proguard-rules.pro b/matrix-sdk-android/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..fa860d8049e259f7494b4a2b0621eecadee2f365 --- /dev/null +++ b/matrix-sdk-android/proguard-rules.pro @@ -0,0 +1,82 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + + +### EVENT BUS ### + +-keepattributes *Annotation* +-keepclassmembers class * { + @org.greenrobot.eventbus.Subscribe <methods>; +} +-keep enum org.greenrobot.eventbus.ThreadMode { *; } + +### MOSHI ### + +# JSR 305 annotations are for embedding nullability information. + +-dontwarn javax.annotation.** + +-keepclasseswithmembers class * { + @com.squareup.moshi.* <methods>; +} + +-keep @com.squareup.moshi.JsonQualifier interface * + +# Enum field names are used by the integrated EnumJsonAdapter. +# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly +# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi. +-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum { + <fields>; + **[] values(); +} + +-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl + +-keepclassmembers class kotlin.Metadata { + public <methods>; +} + +### OKHTTP for Android Studio ### +-keep class okhttp3.Headers { *; } +-keep interface okhttp3.Interceptor.* { *; } + +### OLM JNI ### +-keep class org.matrix.olm.** { *; } + +### Webrtc +-keep class org.webrtc.** { *; } + +### Serializable persisted classes +# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable +-keepnames class * implements java.io.Serializable + +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient <fields>; + !private <fields>; + !private <methods>; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb6e624bced1a6fbcc715ec4645ade8c5622010b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.matrix.android.sdk.test.shared.createTimberTestRule +import org.junit.Rule +import java.io.File + +interface InstrumentedTest { + + @Rule + fun timberTestRule() = createTimberTestRule() + + fun context(): Context { + return ApplicationProvider.getApplicationContext() + } + + fun cacheDir(): File { + return context().cacheDir + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java new file mode 100644 index 0000000000000000000000000000000000000000..a09a6550081c18717351165e11e11bcf044773cf --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk; + +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public final class LiveDataTestObserver<T> implements Observer<T> { + private final List<T> valueHistory = new ArrayList<>(); + private final List<Observer<T>> childObservers = new ArrayList<>(); + + @Deprecated // will be removed in version 1.0 + private final LiveData<T> observedLiveData; + + private CountDownLatch valueLatch = new CountDownLatch(1); + + private LiveDataTestObserver(LiveData<T> observedLiveData) { + this.observedLiveData = observedLiveData; + } + + @Override + public void onChanged(@Nullable T value) { + valueHistory.add(value); + valueLatch.countDown(); + for (Observer<T> childObserver : childObservers) { + childObserver.onChanged(value); + } + } + + public T value() { + assertHasValue(); + return valueHistory.get(valueHistory.size() - 1); + } + + public List<T> valueHistory() { + return Collections.unmodifiableList(valueHistory); + } + + /** + * Disposes and removes observer from observed live data. + * + * @return This Observer + * @deprecated Please use {@link LiveData#removeObserver(Observer)} instead, will be removed in 1.0 + */ + @Deprecated + public LiveDataTestObserver<T> dispose() { + observedLiveData.removeObserver(this); + return this; + } + + public LiveDataTestObserver<T> assertHasValue() { + if (valueHistory.isEmpty()) { + throw fail("Observer never received any value"); + } + + return this; + } + + public LiveDataTestObserver<T> assertNoValue() { + if (!valueHistory.isEmpty()) { + throw fail("Expected no value, but received: " + value()); + } + + return this; + } + + public LiveDataTestObserver<T> assertHistorySize(int expectedSize) { + int size = valueHistory.size(); + if (size != expectedSize) { + throw fail("History size differ; Expected: " + expectedSize + ", Actual: " + size); + } + return this; + } + + public LiveDataTestObserver<T> assertValue(T expected) { + T value = value(); + + if (expected == null && value == null) { + return this; + } + + if (!value.equals(expected)) { + throw fail("Expected: " + valueAndClass(expected) + ", Actual: " + valueAndClass(value)); + } + + return this; + } + + public LiveDataTestObserver<T> assertValue(Function<T, Boolean> valuePredicate) { + T value = value(); + + if (!valuePredicate.apply(value)) { + throw fail("Value not present"); + } + + return this; + } + + public LiveDataTestObserver<T> assertNever(Function<T, Boolean> valuePredicate) { + int size = valueHistory.size(); + for (int valueIndex = 0; valueIndex < size; valueIndex++) { + T value = this.valueHistory.get(valueIndex); + if (valuePredicate.apply(value)) { + throw fail("Value at position " + valueIndex + " matches predicate " + + valuePredicate.toString() + ", which was not expected."); + } + } + + return this; + } + + /** + * Awaits until this TestObserver has any value. + * <p> + * If this TestObserver has already value then this method returns immediately. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver<T> awaitValue() throws InterruptedException { + valueLatch.await(); + return this; + } + + /** + * Awaits the specified amount of time or until this TestObserver has any value. + * <p> + * If this TestObserver has already value then this method returns immediately. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver<T> awaitValue(long timeout, TimeUnit timeUnit) throws InterruptedException { + valueLatch.await(timeout, timeUnit); + return this; + } + + /** + * Awaits until this TestObserver receives next value. + * <p> + * If this TestObserver has already value then it awaits for another one. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver<T> awaitNextValue() throws InterruptedException { + return withNewLatch().awaitValue(); + } + + + /** + * Awaits the specified amount of time or until this TestObserver receives next value. + * <p> + * If this TestObserver has already value then it awaits for another one. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver<T> awaitNextValue(long timeout, TimeUnit timeUnit) throws InterruptedException { + return withNewLatch().awaitValue(timeout, timeUnit); + } + + private LiveDataTestObserver<T> withNewLatch() { + valueLatch = new CountDownLatch(1); + return this; + } + + private AssertionError fail(String message) { + return new AssertionError(message); + } + + private static String valueAndClass(Object value) { + if (value != null) { + return value + " (class: " + value.getClass().getSimpleName() + ")"; + } + return "null"; + } + + public static <T> LiveDataTestObserver<T> create() { + return new LiveDataTestObserver<>(new MutableLiveData<T>()); + } + + public static <T> LiveDataTestObserver<T> test(LiveData<T> liveData) { + LiveDataTestObserver<T> observer = new LiveDataTestObserver<>(liveData); + liveData.observeForever(observer); + return observer; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..d26782d538c16d3edfce204aa4d6b279f2d3dbc5 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Executor; + +public class MainThreadExecutor implements Executor { + + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable runnable) { + handler.post(runnable); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..372ef95be825e54f546f678cee6802de5b8eb3b7 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk + +import okreplay.OkReplayConfig +import okreplay.PermissionRule +import okreplay.RecorderRule +import org.junit.rules.RuleChain +import org.junit.rules.TestRule + +class OkReplayRuleChainNoActivity( + private val configuration: OkReplayConfig) { + + fun get(): TestRule { + return RuleChain.outerRule(PermissionRule(configuration)) + .around(RecorderRule(configuration)) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..4316b09b8990fbf2489d306e3dca32c5b034c961 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk + +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors + +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, + Executors.newSingleThreadExecutor().asCoroutineDispatcher()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cbb5af59112d9f11a3c3d8a6a5cffbb909162df0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class AccountCreationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun createAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun createAccountAndLoginAgainTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + // Log again to the same account + val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) + } + + @Test + fun simpleE2eTest() { + val res = cryptoTestHelper.doE2ETestWithAliceInARoom() + + res.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e2140328e6502a5cbf8ebfd569aa5db09e5bb923 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class ChangePasswordTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + companion object { + private const val NEW_PASSWORD = "this is a new password" + } + + @Test + fun changePasswordTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Change password + commonTestHelper.doSync<Unit> { + session.changePassword(TestConstants.PASSWORD, NEW_PASSWORD, it) + } + + // Try to login with the previous password, it will fail + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + throwable.isInvalidPassword().shouldBeTrue() + + // Try to login with the new password, should work + val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false)) + + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..36d09fb497a2430a1492c23cd3a5a30db6ee5742 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class DeactivateAccountTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun deactivateAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Deactivate the account + commonTestHelper.doSync<Unit> { + session.deactivateAccount(TestConstants.PASSWORD, false, it) + } + + // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + + // Test the error + assertTrue(throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_USER_DEACTIVATED + && throwable.error.message == "This account has been deactivated") + + // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) + val hs = commonTestHelper.createHomeServerConfig() + + commonTestHelper.doSync<LoginFlowResult> { + commonTestHelper.matrix.authenticationService.getLoginFlow(hs, it) + } + + var accountCreationError: Throwable? = null + commonTestHelper.waitWithLatch { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount(session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null, + object : TestMatrixCallback<RegistrationResult>(it, false) { + override fun onFailure(failure: Throwable) { + accountCreationError = failure + super.onFailure(failure) + } + }) + } + + // Test the error + accountCreationError.let { + assertTrue(it is Failure.ServerError + && it.error.code == MatrixError.M_USER_IN_USE) + } + + // No need to close the session, it has been deactivated + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt new file mode 100644 index 0000000000000000000000000000000000000000..df359f2adc42af8c47250ad303e8b3b652b4c2b4 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.common.DaggerTestMatrixComponent +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.network.UserAgentHolder +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.olm.OlmManager +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +/** + * This is the main entry point to the matrix sdk. + * To get the singleton instance, use getInstance static method. + */ +class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + + @Inject internal lateinit var legacySessionImporter: LegacySessionImporter + @Inject internal lateinit var authenticationService: AuthenticationService + @Inject internal lateinit var userAgentHolder: UserAgentHolder + @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + @Inject internal lateinit var olmManager: OlmManager + @Inject internal lateinit var sessionManager: SessionManager + + init { + Monarchy.init(context) + DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this) + if (context.applicationContext !is Configuration.Provider) { + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } + + fun getUserAgent() = userAgentHolder.userAgent + + fun authenticationService(): AuthenticationService { + return authenticationService + } + + fun legacySessionImporter(): LegacySessionImporter { + return legacySessionImporter + } + + companion object { + + private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) + + fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) { + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext, matrixConfiguration) + } + } + + fun getInstance(context: Context): Matrix { + if (isInit.compareAndSet(false, true)) { + val appContext = context.applicationContext + if (appContext is MatrixConfiguration.Provider) { + val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() + instance = Matrix(appContext, matrixConfiguration) + } else { + throw IllegalStateException("Matrix is not initialized properly." + + " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.") + } + } + return instance + } + + fun getSdkVersion(): String { + return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + } + + fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..fdbfa57b5c62cdaedb868c68b2bd2dcf0d2f6086 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -0,0 +1,383 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import android.content.Context +import android.net.Uri +import androidx.lifecycle.Observer +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration +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.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.session.sync.SyncState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import java.util.ArrayList +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * This class exposes methods to be used in common cases + * Registration, login, Sync, Sending messages... + */ +class CommonTestHelper(context: Context) { + + val matrix: Matrix + + fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor + + init { + Matrix.initialize(context, MatrixConfiguration("TestFlavor")) + matrix = Matrix.getInstance(context) + } + + fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) + } + + fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + return logIntoAccount(userId, TestConstants.PASSWORD, testParams) + } + + /** + * Create a Home server configuration, with Http connection allowed for test + */ + fun createHomeServerConfig(): HomeServerConnectionConfig { + return HomeServerConnectionConfig.Builder() + .withHomeServerUri(Uri.parse(TestConstants.TESTS_HOME_SERVER_URL)) + .build() + } + + /** + * This methods init the event stream and check for initial sync + * + * @param session the session to sync + */ + 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) } + + await(lock) + } + + /** + * Sends text messages in a room + * + * @param room the room where to send the messages + * @param message the message to send + * @param nbOfMessages the number of time the message will be sent + */ + fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> { + val timeline = room.createTimeline(null, TimelineSettings(10)) + val sentEvents = ArrayList<TimelineEvent>(nbOfMessages) + val latch = CountDownLatch(1) + val timelineListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + val newMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true } + + if (newMessages.size == nbOfMessages) { + sentEvents.addAll(newMessages) + // Remove listener now, if not at the next update sendEvents could change + timeline.removeListener(this) + latch.countDown() + } + } + } + timeline.start() + timeline.addListener(timelineListener) + for (i in 0 until nbOfMessages) { + room.sendTextMessage(message + " #" + (i + 1)) + } + // Wait 3 second more per message + await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) + timeline.dispose() + + // Check that all events has been created + assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) + + return sentEvents + } + + // PRIVATE METHODS ***************************************************************************** + + /** + * Creates a unique account + * + * @param userNamePrefix the user name prefix + * @param password the password + * @param testParams test params about the session + * @return the session associated with the newly created account + */ + private fun createAccount(userNamePrefix: String, + password: String, + testParams: SessionTestParams): Session { + val session = createAccountAndSync( + userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(), + password, + testParams + ) + assertNotNull(session) + return session + } + + /** + * Logs into an existing account + * + * @param userId the userId to log in + * @param password the password to log in + * @param testParams test params about the session + * @return the session associated with the existing account + */ + fun logIntoAccount(userId: String, + password: String, + testParams: SessionTestParams): Session { + val session = logAccountAndSync(userId, password, testParams) + assertNotNull(session) + return session + } + + /** + * Create an account and a dedicated session + * + * @param userName the account username + * @param password the password + * @param sessionTestParams parameters for the test + */ + private fun createAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync<LoginFlowResult> { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + doSync<RegistrationResult> { + matrix.authenticationService + .getRegistrationWizard() + .createAccount(userName, password, null, it) + } + + // Preform dummy step + val registrationResult = doSync<RegistrationResult> { + matrix.authenticationService + .getRegistrationWizard() + .dummy(it) + } + + assertTrue(registrationResult is RegistrationResult.Success) + val session = (registrationResult as RegistrationResult.Success).session + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Start an account login + * + * @param userName the account username + * @param password the password + * @param sessionTestParams session test params + */ + private fun logAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync<LoginFlowResult> { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + val session = doSync<Session> { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", it) + } + + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Log into the account and expect an error + * + * @param userName the account username + * @param password the password + */ + fun logAccountWithError(userName: String, + password: String): Throwable { + val hs = createHomeServerConfig() + + doSync<LoginFlowResult> { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + var requestFailure: Throwable? = null + waitWithLatch { latch -> + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", object : TestMatrixCallback<Session>(latch, onlySuccessful = false) { + override fun onFailure(failure: Throwable) { + requestFailure = failure + super.onFailure(failure) + } + }) + } + + assertNotNull(requestFailure) + return requestFailure!! + } + + fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + if (predicate(snapshot)) { + latch.countDown() + } + } + } + } + + /** + * Await for a latch and ensure the result is true + * + * @param latch + * @throws InterruptedException + */ + fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { + assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + } + + fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { + GlobalScope.launch { + while (true) { + delay(1000) + if (condition()) { + latch.countDown() + return@launch + } + } + } + } + + fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { + val latch = CountDownLatch(1) + block(latch) + await(latch, timeout) + } + + // 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) + + await(lock) + + assertNotNull(result) + return result!! + } + + /** + * Clear all provided sessions + */ + fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } + + fun signOutAndClose(session: Session) { + doSync<Unit> { session.signOut(true, it) } + session.close() + } +} + +fun List<TimelineEvent>.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean { + return drop(startIndex) + .take(numberOfMessages) + .foldRightIndexed(true) { index, timelineEvent, acc -> + val body = timelineEvent.root.content.toModel<MessageContent>()?.body + val currentMessageSuffix = numberOfMessages - index + acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt new file mode 100644 index 0000000000000000000000000000000000000000..283ddd6fde33e36713c28718de43618a56d166e5 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import org.matrix.android.sdk.api.session.Session + +data class CryptoTestData(val firstSession: Session, + val roomId: String, + val secondSession: Session? = null, + val thirdSession: Session? = null) { + + fun cleanUp(testHelper: CommonTestHelper) { + testHelper.signOutAndClose(firstSession) + secondSession?.let { testHelper.signOutAndClose(it) } + thirdSession?.let { testHelper.signOutAndClose(it) } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..9765d35bc5d08f8c7fa6d02a75966947d418c6ae --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -0,0 +1,424 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.Observer +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import java.util.UUID +import java.util.concurrent.CountDownLatch + +class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { + + private val messagesFromAlice: List<String> = listOf("0 - Hello I'm Alice!", "4 - Go!") + private val messagesFromBob: List<String> = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.") + + private val defaultSessionParams = SessionTestParams(true) + + /** + * @return alice session + */ + fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + + val roomId = mTestHelper.doSync<String> { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + } + + if (encryptedRoom) { + val room = aliceSession.getRoom(roomId)!! + + mTestHelper.doSync<Unit> { + room.enableEncryption(callback = it) + } + } + + return CryptoTestData(aliceSession, roomId) + } + + /** + * @return alice and bob sessions + */ + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val aliceRoom = aliceSession.getRoom(aliceRoomId)!! + + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + val lock1 = CountDownLatch(1) + + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bobSession.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(t: List<RoomSummary>?) { + if (t?.isNotEmpty() == true) { + lock1.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + mTestHelper.doSync<Unit> { + aliceRoom.invite(bobSession.myUserId, callback = it) + } + + mTestHelper.await(lock1) + + val lock = CountDownLatch(1) + + val roomJoinedObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(t: List<RoomSummary>?) { + if (bobSession.getRoom(aliceRoomId) + ?.getRoomMember(aliceSession.myUserId) + ?.membership == Membership.JOIN) { + lock.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(roomJoinedObserver) + } + + mTestHelper.doSync<Unit> { bobSession.joinRoom(aliceRoomId, callback = it) } + + mTestHelper.await(lock) + + // Ensure bob can send messages to the room +// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! +// assertNotNull(roomFromBobPOV.powerLevels) +// assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) + + return CryptoTestData(aliceSession, aliceRoomId, bobSession) + } + + /** + * @return Alice, Bob and Sam session + */ + fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val room = aliceSession.getRoom(aliceRoomId)!! + + val samSession = createSamAccountAndInviteToTheRoom(room) + + // wait the initial sync + SystemClock.sleep(1000) + + return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) + } + + /** + * Create Sam account and invite him in the room. He will accept the invitation + * @Return Sam session + */ + fun createSamAccountAndInviteToTheRoom(room: Room): Session { + val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) + + mTestHelper.doSync<Unit> { + room.invite(samSession.myUserId, null, it) + } + + mTestHelper.doSync<Unit> { + samSession.joinRoom(room.roomId, null, emptyList(), it) + } + + return samSession + } + + /** + * @return Alice and Bob sessions + */ + fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val bobSession = cryptoTestData.secondSession!! + + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + val lock = CountDownLatch(1) + + val bobEventsListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE } + .groupBy { it.root.senderId!! } + + // Alice has sent 2 messages and Bob has sent 3 messages + if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) { + lock.countDown() + } + } + } + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) + bobTimeline.start() + bobTimeline.addListener(bobEventsListener) + + // Alice sends a message + roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) + + // Bob send 3 messages + roomFromBobPOV.sendTextMessage(messagesFromBob[0]) + roomFromBobPOV.sendTextMessage(messagesFromBob[1]) + roomFromBobPOV.sendTextMessage(messagesFromBob[2]) + + // Alice sends a message + roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) + + mTestHelper.await(lock) + + bobTimeline.removeListener(bobEventsListener) + bobTimeline.dispose() + + return cryptoTestData + } + + fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { + assertEquals(EventType.ENCRYPTED, event.type) + assertNotNull(event.content) + + val eventWireContent = event.content.toContent() + assertNotNull(eventWireContent) + + assertNull(eventWireContent["body"]) + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"]) + + assertNotNull(eventWireContent["ciphertext"]) + assertNotNull(eventWireContent["session_id"]) + assertNotNull(eventWireContent["sender_key"]) + + assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"]) + + assertNotNull(event.eventId) + assertEquals(roomId, event.roomId) + assertEquals(EventType.MESSAGE, event.getClearType()) + // TODO assertTrue(event.getAge() < 10000) + + val eventContent = event.toContent() + assertNotNull(eventContent) + assertEquals(clearMessage, eventContent["body"]) + assertEquals(senderSession.myUserId, event.senderId) + } + + fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + return MegolmBackupAuthData( + publicKey = "abcdefg", + signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) + ) + } + + fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { + return MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = createFakeMegolmBackupAuthData() + ) + } + + fun createDM(alice: Session, bob: Session): String { + val roomId = mTestHelper.doSync<String> { + alice.createRoom( + CreateRoomParams().apply { + invitedUserIds.add(bob.myUserId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = true + }, + it + ) + } + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(t: List<RoomSummary>?) { + val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1 + if (indexOfFirst != -1) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + } + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer<List<RoomSummary>> { + override fun onChanged(t: List<RoomSummary>?) { + if (bob.getRoom(roomId) + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) } + } + + return roomId + } + + fun initializeCrossSigning(session: Session) { + mTestHelper.doSync<Unit> { + session.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD + ), it) + } + } + + fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) + assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) + + val requestID = UUID.randomUUID().toString() + val aliceVerificationService = alice.cryptoService().verificationService() + val bobVerificationService = bob.cryptoService().verificationService() + + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID, + roomId, + bob.myUserId, + bob.sessionParams.credentials.deviceId!!, + null) + + // we should reach SHOW SAS on both + var alicePovTx: OutgoingSasVerificationTransaction? = null + var bobPovTx: IncomingSasVerificationTransaction? = null + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + true + } else { + false + } + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + } + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) + + bobPovTx!!.userHasVerifiedShortCode() + alicePovTx!!.userHasVerifiedShortCode() + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9bd9403d270bd961fca162295b0b18e7d802fee --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.common + +import org.matrix.android.sdk.internal.session.TestInterceptor +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import javax.net.ssl.HttpsURLConnection + +/** + * Allows to intercept network requests for test purpose by + * - re-writing the response + * - changing the response code (200/404/etc..). + * - Test delays.. + * + * Basic usage: + * <code> + * val mockInterceptor = MockOkHttpInterceptor() + * mockInterceptor.addRule(MockOkHttpInterceptor.SimpleRule(".well-known/matrix/client", 200, "{}")) + * + * RestHttpClientFactoryProvider.defaultProvider = RestClientHttpClientFactory(mockInterceptor) + * AutoDiscovery().findClientConfig("matrix.org", <callback>) + * </code> + */ +class MockOkHttpInterceptor : TestInterceptor { + + private var rules: ArrayList<Rule> = ArrayList() + + fun addRule(rule: Rule) { + rules.add(rule) + } + + fun clearRules() { + rules.clear() + } + + override var sessionId: String? = null + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + rules.forEach { rule -> + if (originalRequest.url.toString().contains(rule.match)) { + rule.process(originalRequest)?.let { + return it + } + } + } + + return chain.proceed(originalRequest) + } + + abstract class Rule(val match: String) { + abstract fun process(originalRequest: Request): Response? + } + + /** + * Simple rule that reply with the given body for any request that matches the match param + */ + class SimpleRule(match: String, + private val code: Int = HttpsURLConnection.HTTP_OK, + private val body: String = "{}") : Rule(match) { + + override fun process(originalRequest: Request): Response? { + return Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(originalRequest) + .message("mocked answer") + .body(body.toResponseBody(null)) + .code(code) + .build() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..287cafcdfd67f67ee7a01a577694364ff0f4914b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +data class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..d972ad621c2549ce4da8350e16a9c72a2a937718 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.fail + +/** + * Compare two lists and their content + */ +fun assertListEquals(list1: List<Any>?, list2: List<Any>?) { + if (list1 == null) { + assertNull(list2) + } else { + assertNotNull(list2) + + assertEquals("List sizes must match", list1.size, list2!!.size) + + for (i in list1.indices) { + assertEquals("Elements at index $i are not equal", list1[i], list2[i]) + } + } +} + +/** + * Compare two maps and their content + */ +fun assertDictEquals(dict1: Map<String, Any>?, dict2: Map<String, Any>?) { + if (dict1 == null) { + assertNull(dict2) + } else { + assertNotNull(dict2) + + assertEquals("Map sizes must match", dict1.size, dict2!!.size) + + for (i in dict1.keys) { + assertEquals("Values for key $i are not equal", dict1[i], dict2[i]) + } + } +} + +/** + * Compare two byte arrays content. + * Note that if the arrays have not the same size, it also fails. + */ +fun assertByteArrayNotEqual(a1: ByteArray, a2: ByteArray) { + if (a1.size != a2.size) { + fail("Arrays have not the same size.") + } + + for (index in a1.indices) { + if (a1[index] != a2[index]) { + // Difference found! + return + } + } + + fail("Arrays are equals.") +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..cbfc9bbbf6d2351cced60ccd443927774f1109c1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import android.os.Debug + +object TestConstants { + + const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" + + // Time out to use when waiting for server response. 20s + private const val AWAIT_TIME_OUT_MILLIS = 20_000 + + // Time out to use when waiting for server response, when the debugger is connected. 10 minutes + private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 + + const val USER_ALICE = "Alice" + const val USER_BOB = "Bob" + const val USER_SAM = "Sam" + + const val PASSWORD = "password" + + val timeOutMillis: Long + get() = if (Debug.isDebuggerConnected()) { + // Wait more + AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS.toLong() + } else { + AWAIT_TIME_OUT_MILLIS.toLong() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..800c6ae7e0d201f2e609e89e71a6d8c152ae8f96 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import androidx.annotation.CallSuper +import org.matrix.android.sdk.api.MatrixCallback +import org.junit.Assert.fail +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() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..50290e1d632498ca9878f3b43862ebeb0cd5eaf4 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.auth.AuthModule +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.MatrixModule +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.di.NetworkModule + +@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class]) +@MatrixScope +internal interface TestMatrixComponent : MatrixComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context, + @BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3b11d65cc6a5c4d3b5f7066bc388d465af2bed5 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.internal.di.MatrixComponent + +@Module +internal abstract class TestModule { + @Binds + abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..80467d91f4be1b5beda209c0f625186620d32adc --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor + +@Module +internal object TestNetworkModule { + + val interceptors = ArrayList<TestInterceptor>() + + fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId } + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? { + return MockOkHttpInterceptor().also { + interceptors.add(it) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..05dbc40e1e42b545c37f5d7608322d783dcbb44d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.os.MemoryFile +import android.util.Base64 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.io.ByteArrayInputStream +import java.io.InputStream + +/** + * Unit tests AttachmentEncryptionTest. + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AttachmentEncryptionTest { + + private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String { + val `in` = Base64.decode(input, Base64.DEFAULT) + + val inputStream: InputStream + + inputStream = if (`in`.isEmpty()) { + ByteArrayInputStream(`in`) + } else { + val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size) + memoryFile.outputStream.write(`in`) + memoryFile.inputStream + } + + val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) + + assertNotNull(decryptedStream) + + val buffer = ByteArray(100) + + val len = decryptedStream!!.read(buffer) + + decryptedStream.close() + + return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") + } + + @Test + fun checkDecrypt1() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "AAAAAAAAAAAAAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("", checkDecryption("", encryptedFileInfo)) + } + + @Test + fun checkDecrypt2() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "YzF08lARDdOCzJpzuSwsjTNlQc4pHxpdHcXiD/wpK6k"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("SGVsbG8sIFdvcmxk", checkDecryption("5xJZTt5cQicm+9f4", encryptedFileInfo)) + } + + @Test + fun checkDecrypt3() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "IOq7/dHHB+mfHfxlRY5XMeCWEwTPmlf4cJcgrkf6fVU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", + encryptedFileInfo)) + } + + @Test + fun checkDecrypt4() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "LYG/orOViuFwovJpv2YMLSsmVKwLt7pY3f8SYM7KU5E"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "/////////////////////w", + url = "dummyUrl" + ) + + assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", + encryptedFileInfo)) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..261c0903f0107ad86ca97ee323220a6197245122 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmConfiguration +import kotlin.random.Random + +internal class CryptoStoreHelper { + + fun createStore(): IMXCryptoStore { + return RealmCryptoStore( + realmConfiguration = RealmConfiguration.Builder() + .name("test.realm") + .modules(RealmCryptoStoreModule()) + .build(), + crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), + credentials = createCredential()) + } + + fun createCredential() = Credentials( + userId = "userId_" + Random.nextInt(), + homeServer = "http://matrix.org", + accessToken = "access_token", + refreshToken = null, + deviceId = "deviceId_sample" + ) +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..79477e3a4d205e9c8155c0e9a4877c85c894988c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import io.realm.Realm +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmManager +import org.matrix.olm.OlmSession + +private const val DUMMY_DEVICE_KEY = "DeviceKey" + +@RunWith(AndroidJUnit4::class) +class CryptoStoreTest : InstrumentedTest { + + private val cryptoStoreHelper = CryptoStoreHelper() + + @Before + fun setup() { + Realm.init(context()) + } + +// @Test +// fun test_metadata_realm_ok() { +// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() +// +// assertFalse(cryptoStore.hasData()) +// +// cryptoStore.open() +// +// assertEquals("deviceId_sample", cryptoStore.getDeviceId()) +// +// assertTrue(cryptoStore.hasData()) +// +// // Cleanup +// cryptoStore.close() +// cryptoStore.deleteStore() +// } + + @Test + fun test_lastSessionUsed() { + // Ensure Olm is initialized + OlmManager() + + val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() + + assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + val olmAccount1 = OlmAccount().apply { + generateOneTimeKeys(1) + } + + val olmSession1 = OlmSession().apply { + initOutboundSession(olmAccount1, + olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], + olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + } + + val sessionId1 = olmSession1.sessionIdentifier() + val olmSessionWrapper1 = OlmSessionWrapper(olmSession1) + + cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) + + assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + val olmAccount2 = OlmAccount().apply { + generateOneTimeKeys(1) + } + + val olmSession2 = OlmSession().apply { + initOutboundSession(olmAccount2, + olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], + olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + } + + val sessionId2 = olmSession2.sessionIdentifier() + val olmSessionWrapper2 = OlmSessionWrapper(olmSession2) + + cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) + + // Ensure sessionIds are distinct + assertNotEquals(sessionId1, sessionId2) + + // Note: we cannot be sure what will be the result of getLastUsedSessionId() here + + olmSessionWrapper2.onMessageReceived() + cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) + + // sessionId2 is returned now + assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + Thread.sleep(2) + + olmSessionWrapper1.onMessageReceived() + cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) + + // sessionId1 is returned now + assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + // Cleanup + olmSession1.releaseSession() + olmSession2.releaseSession() + + olmAccount1.releaseAccount() + olmAccount2.releaseAccount() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ee79c2e1e4e097a227e56c30aec7d2c998d8e21 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * Unit tests ExportEncryptionTest. + */ +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExportEncryptionTest { + + @Test + fun checkExportError1() { + val password = "password" + val input = "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError2() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError3() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + + " AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + + " cissyYBxjsfsAn\n" + + " -----END MEGOLM SESSION DATA-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportDecrypt1() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + val expectedString = "plain" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt1() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt2() { + val password = "betterpassword" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" + "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + val expectedString = "Hello, World" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt2() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt3() { + val password = "SWORDFISH" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" + "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt3() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt1() { + val password = "password" + val expectedString = "plain" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt1() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt2() { + val password = "betterpassword" + val expectedString = "Hello, World" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt2() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt3() { + val password = "SWORDFISH" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt3() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt4() { + val password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt4() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5e71f394467ec2292b0af041bf91cfa56ea0ae2 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.amshove.kluent.shouldBe +import org.junit.Assert +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.olm.OlmSession +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +/** + * Ref: + * - https://github.com/matrix-org/matrix-doc/pull/1719 + * - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages + * - https://github.com/matrix-org/matrix-js-sdk/pull/780 + * - https://github.com/matrix-org/matrix-ios-sdk/pull/778 + * - https://github.com/matrix-org/matrix-ios-sdk/pull/784 + */ +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class UnwedgingTest : InstrumentedTest { + + private lateinit var messagesReceivedByBob: List<TimelineEvent> + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Before + fun init() { + messagesReceivedByBob = emptyList() + } + + /** + * - Alice & Bob in a e2e room + * - Alice sends a 1st message with a 1st megolm session + * - Store the olm session between A&B devices + * - Alice sends a 2nd message with a 2nd megolm session + * - Simulate Alice using a backup of her OS and make her crypto state like after the first message + * - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + * + * What Bob must see: + * -> No issue with the 2 first messages + * -> The third event must fail to decrypt at first because Bob the olm session is wedged + * -> This is automatically fixed after SDKs restarted the olm session + */ + @Test + fun testUnwedging() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val bobSession = cryptoTestData.secondSession!! + + val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + + // bobSession.cryptoService().setWarnOnUnknownDevices(false) + // aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) + bobTimeline.start() + + val bobFinalLatch = CountDownLatch(1) + val bobHasThreeDecryptedEventsListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages") + if (decryptedEventReceivedByBob.size == 3) { + if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + bobFinalLatch.countDown() + } + } + } + } + bobTimeline.addListener(bobHasThreeDecryptedEventsListener) + + var latch = CountDownLatch(1) + var bobEventsListener = createEventListener(latch, 1) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + // - Alice sends a 1st message with a 1st megolm session + roomFromAlicePOV.sendTextMessage("First message") + + // Wait for the message to be received by Bob + mTestHelper.await(latch) + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 1 + val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! + + // - Store the olm session between A&B devices + // Let us pickle our session with bob here so we can later unpickle it + // and wedge our session. + val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) + sessionIdsForBob!!.size shouldBe 1 + val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! + + val oldSession = serializeForRealm(olmSession.olmSession) + + aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + Thread.sleep(6_000) + + latch = CountDownLatch(1) + bobEventsListener = createEventListener(latch, 2) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") + // - Alice sends a 2nd message with a 2nd megolm session + roomFromAlicePOV.sendTextMessage("Second message") + + // Wait for the message to be received by Bob + mTestHelper.await(latch) + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 2 + // Session should have changed + val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! + Assert.assertNotEquals(firstMessageSession, secondMessageSession) + + // Let us wedge the session now. Set crypto state like after the first message + Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") + + aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!) + Thread.sleep(6_000) + + // Force new session, and key share + aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + + // Wait for the message to be received by Bob + mTestHelper.waitWithLatch { + bobEventsListener = createEventListener(it, 3) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") + // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + roomFromAlicePOV.sendTextMessage("Third message") + // Bob should not be able to decrypt, because the session key could not be sent + } + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 3 + + val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! + Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") + Assert.assertNotEquals(secondMessageSession, thirdMessageSession) + + Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType()) + Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) + Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) + // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged + mTestHelper.await(bobFinalLatch) + bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) + + // It's a trick to force key request on fail to decrypt + mTestHelper.doSync<Unit> { + bobSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + // Wait until we received back the key + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + // we should get back the key and be able to decrypt + val result = tryThis { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") + } + Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") + result != null + } + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(mTestHelper) + } + + private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + + if (messagesReceivedByBob.size == expectedNumberOfMessages) { + latch.countDown() + } + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9467e861dba72a44d53fbb17fca4449302ce46f7 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldBeTrue +import org.junit.Test + +@Suppress("SpellCheckingInspection") +class ExtensionsKtTest { + + @Test + fun testComparingBase64StringWithOrWithoutPadding() { + // Without padding + "NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic".fromBase64()).shouldBeTrue() + // With padding + "NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic=".fromBase64()).shouldBeTrue() + } + + @Test + fun testBadBase64() { + "===".fromBase64Safe().shouldBeNull() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..09f14032d01901e7e7fbfef9eb043df51ff41427 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -0,0 +1,161 @@ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class XSigningTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_InitializeAndStoreKeys() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + mTestHelper.doSync<Unit> { + aliceSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + val masterPubKey = myCrossSigningKeys?.masterKey() + assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) + val selfSigningKey = myCrossSigningKeys?.selfSigningKey() + assertNotNull("SelfSigned key should be stored", selfSigningKey?.unpaddedBase64PublicKey) + val userKey = myCrossSigningKeys?.userKey() + assertNotNull("User key should be stored", userKey?.unpaddedBase64PublicKey) + + assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) + + assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_CrossSigningCheckBobSeesTheKeys() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + + // Check that alice can see bob keys + mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) + assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) + assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) + + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + + assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + } + + @Test + fun test_CrossSigningTestAliceTrustBobNewDevice() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + + // Check that alice can see bob keys + val bobUserId = bobSession.myUserId + mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) + assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) + + mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + + // Now bobs logs in on a new device and verifies it + // We will want to test that in alice POV, this new device would be trusted by cross signing + + val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) + val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! + + // Check that bob first session sees the new login + val data = mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } + + if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Bob should see the new device") + } + + val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId) + assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) + + // Manually mark it as trusted from first session + mTestHelper.doSync<Unit> { + bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) + } + + // Now alice should cross trust bob's second device + val data2 = mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } + + // check that the device is seen + if (data2.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Alice should see the new device") + } + + val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c1a88dc7599852004825436b40b02e9542e7f0d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.gossiping + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +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.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import junit.framework.TestCase.fail +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeyShareTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_DoNotSelfShareIfNotTrusted() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Create an encrypted room and add a message + val roomId = mTestHelper.doSync<String> { + aliceSession.createRoom( + CreateRoomParams().apply { + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() + }, + it + ) + } + val room = aliceSession.getRoom(roomId) + assertNotNull(room) + Thread.sleep(4_000) + assertTrue(room?.isEncrypted() == true) + val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId + + // Open a new sessionx + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + val roomSecondSessionPOV = aliceSession2.getRoom(roomId) + + val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId) + assertNotNull(receivedEvent) + assert(receivedEvent!!.isEncrypted()) + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + // Try to request + aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) + + val waitLatch = CountDownLatch(1) + val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId + + var outGoingRequestId: String? = null + + mTestHelper.retryPeriodicallyWithLatch(waitLatch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + .filter { req -> + // filter out request that was known before + !outgoingRequestsBefore.any { req.requestId == it.requestId } + } + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } + } + mTestHelper.await(waitLatch) + + Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") + + val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + + // We should have a new request + Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size) + Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId }) + + // The first session should see an incoming request + // the request should be refused, because the device is not trusted + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") + Log.v("TEST", "=========================") + it.forEach { keyRequest -> + Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}") + } + Log.v("TEST", "=========================") + } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming?.state == GossipingRequestState.REJECTED + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + // Mark the device as trusted + aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession2.sessionParams.deviceId ?: "") + + // Re request + aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("TEST", "Incoming request Session 1") + Log.v("TEST", "=========================") + it.forEach { + Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}") + } + Log.v("TEST", "=========================") + + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED } + } + } + } + + Thread.sleep(6_000) + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let { + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } + } + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } catch (failure: Throwable) { + fail("should have been able to decrypt") + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(aliceSession2) + } + + @Test + fun test_ShareSSSSSecret() { + val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + mTestHelper.doSync<Unit> { + aliceSession1.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession1.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + // Also bootstrap keybackup on first session + val creationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> { + aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = mTestHelper.doSync<KeysVersion> { + aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true)) + + val aliceVerificationService1 = aliceSession1.cryptoService().verificationService() + val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() + + // force keys download + mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it) + } + mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it) + } + + var session1ShortCode: String? = null + var session2ShortCode: String? = null + + aliceVerificationService1.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.OnStarted) { + (tx as IncomingSasVerificationTransaction).performAccept() + } + if (tx.state == VerificationTxState.ShortCodeReady) { + session1ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) + tx.userHasVerifiedShortCode() + } + } + } + }) + + aliceVerificationService2.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.ShortCodeReady) { + session2ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) + tx.userHasVerifiedShortCode() + } + } + } + }) + + val txId = "m.testVerif12" + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId + ?: "", txId) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true + } + } + + assertNotNull(session1ShortCode) + Log.d("#TEST", "session1ShortCode: $session1ShortCode") + assertNotNull(session2ShortCode) + Log.d("#TEST", "session2ShortCode: $session2ShortCode") + assertEquals(session1ShortCode, session2ShortCode) + + // SSK and USK private keys should have been shared + + mTestHelper.waitWithLatch(60_000) { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}") + aliceSession2.cryptoService().crossSigningService().canCrossSign() + } + } + + // Test that key backup key has been shared to + mTestHelper.waitWithLatch(60_000) { latch -> + val keysBackupService = aliceSession2.cryptoService().keysBackupService() + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}") + keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey + } + } + + mTestHelper.signOutAndClose(aliceSession1) + mTestHelper.signOutAndClose(aliceSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt new file mode 100644 index 0000000000000000000000000000000000000000..715448721975fb6899abb8850ea6cb5dd257227a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.gossiping + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.MockOkHttpInterceptor +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class WithHeldTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_WithHeldUnverifiedReason() { + // ============================= + // ARRANGE + // ============================= + + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Initialize cross signing on both + mCryptoTestHelper.initializeCrossSigning(aliceSession) + mCryptoTestHelper.initializeCrossSigning(bobSession) + + val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession) + mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId) + + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + // ============================= + // ACT + // ============================= + + // Alice decide to not send to unverified sessions + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + + // await for bob unverified session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null + } + } + + val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! + + // ============================= + // ASSERT + // ============================= + + // Bob should not be able to decrypt because the keys is withheld + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + // enable back sending to unverified + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) + + val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE + } + } + + // Previous message should still be undecryptable (partially withheld session) + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobUnverifiedSession) + } + + @Test + fun test_WithHeldNoOlm() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession) + + // Simulate no OTK + aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ + { "one_time_keys" : {} } + """ + )) + Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + // await for bob session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null + } + } + + // Previous message should still be undecryptable (partially withheld session) + val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) + try { + // .. might need to wait a bit for stability? + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + } + + // Ensure that alice has marked the session to be shared with bob + val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!! + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) + // Add a new device for bob + + aliceInterceptor.clearRules() + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true)) + // send a second message + val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId + + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null + } + } + + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) + + aliceInterceptor.clearRules() + testData.cleanUp(mTestHelper) + mTestHelper.signOutAndClose(bobSecondSession) + } + + @Test + fun test_WithHeldKeyRequest() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + mTestHelper.signOutAndClose(bobSession) + + // Create a new session for bob + + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // initialize to force request keys if missing + mCryptoTestHelper.initializeCrossSigning(bobSecondSession) + + // Trust bob second device from Alice POV + aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + + var sessionId: String? = null + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { + // try to decrypt and force key request + tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } + } + sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId + timeLineEvent != null + } + } + + // Check that bob second session requested the key + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f38b55beba889f74be1977317062ac8bc5c9943a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.common.assertByteArrayNotEqual +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.olm.OlmManager +import org.matrix.olm.OlmPkDecryption + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeysBackupPasswordTest : InstrumentedTest { + + @Before + fun ensureLibLoaded() { + OlmManager() + } + + /** + * Check KeysBackupPassword utilities + */ + @Test + fun passwordConverter_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertArrayEquals(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check generatePrivateKeyWithPassword progress listener behavior + */ + @Test + fun passwordConverter_progress_ok() { + val progressValues = ArrayList<Int>(101) + var lastTotal = 0 + + generatePrivateKeyWithPassword(PASSWORD, object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + if (!progressValues.contains(progress)) { + progressValues.add(progress) + } + + lastTotal = total + } + }) + + assertEquals(100, lastTotal) + + // Ensure all values are here + assertEquals(101, progressValues.size) + + for (i in 0..100) { + assertTrue(progressValues[i] == i) + } + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badPassword_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad password + val retrievedPrivateKey = retrievePrivateKeyWithPassword(BAD_PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badIteration_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + 500_001) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad salt + */ + @Test + fun passwordConverter_badSalt_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + BAD_SALT, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check [retrievePrivateKeyWithPassword] with data coming from another platform (RiotWeb). + */ + @Test + fun passwordConverter_crossPlatform_ok() { + val password = "This is a passphrase!" + val salt = "TO0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + val iteration = 500_000 + + val retrievedPrivateKey = retrievePrivateKeyWithPassword(password, salt, iteration) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + + // Data from RiotWeb + val privateKeyBytes = byteArrayOf( + 116.toByte(), 224.toByte(), 229.toByte(), 224.toByte(), 9.toByte(), 3.toByte(), 178.toByte(), 162.toByte(), + 120.toByte(), 23.toByte(), 108.toByte(), 218.toByte(), 22.toByte(), 61.toByte(), 241.toByte(), 200.toByte(), + 235.toByte(), 173.toByte(), 236.toByte(), 100.toByte(), 115.toByte(), 247.toByte(), 33.toByte(), 132.toByte(), + 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte()) + + assertArrayEquals(privateKeyBytes, retrievedPrivateKey) + } + + companion object { + private const val PASSWORD = "password" + private const val BAD_PASSWORD = "passw0rd" + + private const val BAD_SALT = "AA0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt new file mode 100644 index 0000000000000000000000000000000000000000..29a0b5ffd6b7b8aa5c051e6c517cfc4869c89c0a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestData +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 + +/** + * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] + */ +data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, + val aliceKeys: List<OlmInboundGroupSessionWrapper2>, + val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, + val aliceSession2: Session) { + fun cleanUp(testHelper: CommonTestHelper) { + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(aliceSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..aef97d568765685047ac7ff3ce71dd352c521a08 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -0,0 +1,1099 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.ArrayList +import java.util.Collections +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeysBackupTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + private val mKeysBackupTestHelper = KeysBackupTestHelper(mTestHelper, mCryptoTestHelper) + + /** + * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + * - Check backup keys after having marked one as backed up + * - Reset keys backup markers + */ + @Test + fun roomKeysTest_testBackupStore_ok() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store + val sessions = cryptoStore.inboundGroupSessionsToBackup(100) + val sessionsCount = sessions.size + + assertFalse(sessions.isEmpty()) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + // - Check backup keys after having marked one as backed up + val session = sessions[0] + + cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session)) + + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount - 1, sessions2.size) + + // - Reset keys backup markers + cryptoStore.resetBackupMarkers() + + val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount, sessions3.size) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check that prepareKeysBackupVersionWithPassword returns valid data + */ + @Test + fun prepareKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) + + assertNotNull(bobSession.cryptoService().keysBackupService()) + + val keysBackup = bobSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val megolmBackupCreationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> { + keysBackup.prepareKeysBackupVersion(null, null, it) + } + + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm) + assertNotNull(megolmBackupCreationInfo.authData) + assertNotNull(megolmBackupCreationInfo.authData!!.publicKey) + assertNotNull(megolmBackupCreationInfo.authData!!.signatures) + assertNotNull(megolmBackupCreationInfo.recoveryKey) + + stateObserver.stopAndCheckStates(null) + mTestHelper.signOutAndClose(bobSession) + } + + /** + * Test creating a keys backup version and check that createKeysBackupVersion() returns valid data + */ + @Test + fun createKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) + + val keysBackup = bobSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val megolmBackupCreationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> { + keysBackup.prepareKeysBackupVersion(null, null, it) + } + + assertFalse(keysBackup.isEnabled) + + // Create the version + mTestHelper.doSync<KeysVersion> { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + mTestHelper.signOutAndClose(bobSession) + } + + /** + * - Check that createKeysBackupVersion() launches the backup + * - Check the backup completes + */ + @Test + fun backupAfterCreateKeysBackupVersionTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val latch = CountDownLatch(1) + + assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + val stateObserver = StateObserver(keysBackup, latch, 5) + + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + mTestHelper.await(latch) + + val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + + assertEquals(2, nbOfKeys) + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + // Check the several backup state changes + stateObserver.stopAndCheckStates( + listOf( + KeysBackupState.Enabling, + KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp + ) + ) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check that backupAllGroupSessions() returns valid data + */ + @Test + fun backupAllGroupSessionsTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Check that backupAllGroupSessions returns valid data + val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + + assertEquals(2, nbOfKeys) + + var lastBackedUpKeysProgress = 0 + + mTestHelper.doSync<Unit> { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + assertEquals(nbOfKeys, total) + lastBackedUpKeysProgress = progress + } + }, it) + } + + assertEquals(nbOfKeys, lastBackedUpKeysProgress) + + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check encryption and decryption of megolm keys in the backup. + * - Pick a megolm key + * - Check [MXKeyBackup encryptGroupSession] returns stg + * - Check [MXKeyBackup pkDecryptionFromRecoveryKey] is able to create a OLMPkDecryption + * - Check [MXKeyBackup decryptKeyBackupData] returns stg + * - Compare the decrypted megolm key with the original one + */ + @Test + fun testEncryptAndDecryptKeysBackupData() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService + + val stateObserver = StateObserver(keysBackup) + + // - Pick a megolm key + val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo + + // - Check encryptGroupSession() returns stg + val keyBackupData = keysBackup.encryptGroupSession(session) + assertNotNull(keyBackupData) + assertNotNull(keyBackupData.sessionData) + + // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption + val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) + assertNotNull(decryption) + // - Check decryptKeyBackupData() returns stg + val sessionData = keysBackup + .decryptKeyBackupData(keyBackupData, + session.olmInboundGroupSession!!.sessionIdentifier(), + cryptoTestData.roomId, + decryption!!) + assertNotNull(sessionData) + // - Compare the decrypted megolm key with the original one + mKeysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + */ + @Test + fun restoreKeysBackupTest() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Restore the e2e backup from the homeserver + val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * + * This is the same as `testRestoreKeyBackup` but this test checks that pending key + * share requests are cancelled. + * + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - *** Check the SDK sent key share requests + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + * - *** There must be no more pending key share requests + */ +// @Test +// fun restoreKeysBackupAndKeyShareRequestTest() { +// fail("Check with Valere for this test. I think we do not send key share request") +// +// val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) +// +// // - Check the SDK sent key share requests +// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val unsentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequest != null || sentRequest != null) +// +// // - Restore the e2e backup from the homeserver +// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { +// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, +// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, +// null, +// null, +// null, +// it +// ) +// } +// +// mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) +// +// // - There must be no more pending key share requests +// val unsentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) +// +// testData.cleanUp(mTestHelper) +// } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device + mTestHelper.doSync<Unit> { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + true, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync<KeysBackupVersionTrust> { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the recovery key + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device with the recovery key + mTestHelper.doSync<Unit> { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync<KeysBackupVersionTrust> { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong recovery key + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Try to trust the backup from the new device with a wrong recovery key + val latch = CountDownLatch(1) + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "Bad recovery key", + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the password + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithPasswordTest() { + val password = "Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device with the password + mTestHelper.doSync<Unit> { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync<KeysBackupVersionTrust> { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong password + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongPasswordTest() { + val password = "Password" + val badPassword = "Bad Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Try to trust the backup from the new device with a wrong password + val latch = CountDownLatch(1) + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + badPassword, + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong recovery key + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongRecoveryKeyTest() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a wrong recovery key + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the password + * - Restore must be successful + */ + @Test + fun testBackupWithPassword() { + val password = "password" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the password + val steps = ArrayList<StepProgressListener.Step>() + + val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + null, + null, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + steps.add(step) + } + }, + it + ) + } + + // Check steps + assertEquals(105, steps.size) + + for (i in 0..100) { + assertTrue(steps[i] is StepProgressListener.Step.ComputingKey) + assertEquals(i, (steps[i] as StepProgressListener.Step.ComputingKey).progress) + assertEquals(100, (steps[i] as StepProgressListener.Step.ComputingKey).total) + } + + assertTrue(steps[101] is StepProgressListener.Step.DownloadingKey) + + // 2 Keys to import, value will be 0%, 50%, 100% + for (i in 102..104) { + assertTrue(steps[i] is StepProgressListener.Step.ImportingKey) + assertEquals(100, (steps[i] as StepProgressListener.Step.ImportingKey).total) + } + + assertEquals(0, (steps[102] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong password + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongPasswordTest() { + val password = "password" + val wrongPassword = "passw0rd" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Try to restore the e2e backup with a wrong password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + wrongPassword, + null, + null, + null, + object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the recovery key. + * - Restore must be successful + */ + @Test + fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { + val password = "password" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the recovery key. + val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - Try to restore the e2e backup with a password + * - It must fail + */ + @Test + fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "password", + null, + null, + null, + object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Create a backup version + * - Check the returned KeysVersionResult is trusted + */ + @Test + fun testIsKeysBackupTrusted() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Do an e2e backup to the homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Get key backup version from the home server + val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> { + keysBackup.getCurrentVersion(it) + } + + // - Check the returned KeyBackupVersion is trusted + val keysBackupVersionTrust = mTestHelper.doSync<KeysBackupVersionTrust> { + keysBackup.getKeysBackupTrust(keysVersionResult!!, it) + } + + assertNotNull(keysBackupVersionTrust) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(1, keysBackupVersionTrust.signatures.size) + + val signature = keysBackupVersionTrust.signatures[0] + assertTrue(signature.valid) + assertNotNull(signature.device) + assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check backup starts automatically if there is an existing and compatible backup + * version on the homeserver. + * - Create a backup version + * - Restart alice session + * -> The new alice session must back up to the same version + */ + @Test + fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() { + fail("This test still fail. To investigate") + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // - Restart alice session + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + cryptoTestData.cleanUp(mTestHelper) + + val keysBackup2 = aliceSession2.cryptoService().keysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + // -> The new alice session must back up to the same version + val latch = CountDownLatch(1) + var count = 0 + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch.countDown() + } + } + } + }) + mTestHelper.await(latch) + + assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + mTestHelper.signOutAndClose(aliceSession2) + } + + /** + * Check WrongBackUpVersion state + * + * - Make alice back up her keys to her homeserver + * - Create a new backup with fake data on the homeserver + * - Make alice back up all her keys again + * -> That must fail and her backup state must be WrongBackUpVersion + */ + @Test + fun testBackupWhenAnotherBackupWasCreated() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + // Wait for keys backup to be finished + val latch0 = CountDownLatch(1) + var count = 0 + keysBackup.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch0.countDown() + } + } + } + }) + + // - Make alice back up her keys to her homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + mTestHelper.await(latch0) + + // - Create a new backup with fake data on the homeserver, directly using the rest client + val megolmBackupCreationInfo = mCryptoTestHelper.createFakeMegolmBackupCreationInfo() + mTestHelper.doSync<KeysVersion> { + (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) + } + + // Reset the store backup status for keys + (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() + + // - Make alice back up all her keys again + val latch2 = CountDownLatch(1) + keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) + mTestHelper.await(latch2) + + // -> That must fail and her backup state must be WrongBackUpVersion + assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state) + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver + * - Log Alice on a new device + * - Post a message to have a new megolm session + * - Try to backup all + * -> It must fail. Backup state must be NotTrusted + * - Validate the old device from the new one + * -> Backup should automatically enable on the new device + * -> It must use the same backup version + * - Try to backup all again + * -> It must success + */ + @Test + fun testBackupAfterVerifyingADevice() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Make alice back up her keys to her homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Wait for keys backup to finish by asking again to backup keys. + mTestHelper.doSync<Unit> { + keysBackup.backupAllGroupSessions(null, it) + } + + val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! + val oldKeyBackupVersion = keysBackup.currentBackupVersion + val aliceUserId = cryptoTestData.firstSession.myUserId + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + // - Post a message to have a new megolm session + aliceSession2.cryptoService().setWarnOnUnknownDevices(false) + + val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!! + + mTestHelper.sendTextMessage(room2, "New key", 1) + + // - Try to backup all in aliceSession2, it must fail + val keysBackup2 = aliceSession2.cryptoService().keysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + var isSuccessful = false + val latch2 = CountDownLatch(1) + keysBackup2.backupAllGroupSessions( + null, + object : TestMatrixCallback<Unit>(latch2, false) { + override fun onSuccess(data: Unit) { + isSuccessful = true + super.onSuccess(data) + } + }) + mTestHelper.await(latch2) + + assertFalse(isSuccessful) + + // Backup state must be NotTrusted + assertEquals(KeysBackupState.NotTrusted, keysBackup2.state) + assertFalse(keysBackup2.isEnabled) + + // - Validate the old device from the new one + aliceSession2.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession2.myUserId, oldDeviceId) + + // -> Backup should automatically enable on the new device + val latch4 = CountDownLatch(1) + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.state == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch4.countDown() + } + } + }) + mTestHelper.await(latch4) + + // -> It must use the same backup version + assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) + + mTestHelper.doSync<Unit> { + aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) + } + + // -> It must success + assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + mTestHelper.signOutAndClose(aliceSession2) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Delete the backup + */ + @Test + fun deleteKeysBackupTest() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // Delete the backup + mTestHelper.doSync<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + + // Backup is now disabled + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..f31e67b0e8d7d5063d437773b4a44401b5adb03e --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.common.SessionTestParams + +object KeysBackupTestConstants { + val defaultSessionParams = SessionTestParams(withInitialSync = false) + val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true) +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..f84a90708c7a1d83d5708e60024c54149adf27bd --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.assertDictEquals +import org.matrix.android.sdk.common.assertListEquals +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.junit.Assert +import java.util.concurrent.CountDownLatch + +class KeysBackupTestHelper( + private val mTestHelper: CommonTestHelper, + private val mCryptoTestHelper: CryptoTestHelper) { + + /** + * Common initial condition + * - Do an e2e backup to the homeserver + * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted) + * + * @param password optional password + */ + fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100) + + // - Do an e2e backup to the homeserver + val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password) + + var lastProgress = 0 + var lastTotal = 0 + mTestHelper.doSync<Unit> { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + lastProgress = progress + lastTotal = total + } + }, it) + } + + Assert.assertEquals(2, lastProgress) + Assert.assertEquals(2, lastTotal) + + val aliceUserId = cryptoTestData.firstSession.myUserId + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + // Test check: aliceSession2 has no keys at login + Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + + // Wait for backup state to be NotTrusted + waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) + + stateObserver.stopAndCheckStates(null) + + return KeysBackupScenarioData(cryptoTestData, + aliceKeys, + prepareKeysBackupDataResult, + aliceSession2) + } + + fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService, + password: String? = null): PrepareKeysBackupDataResult { + val stateObserver = StateObserver(keysBackup) + + val megolmBackupCreationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> { + keysBackup.prepareKeysBackupVersion(password, null, it) + } + + Assert.assertNotNull(megolmBackupCreationInfo) + + Assert.assertFalse(keysBackup.isEnabled) + + // Create the version + val keysVersion = mTestHelper.doSync<KeysVersion> { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + Assert.assertNotNull(keysVersion.version) + + // Backup must be enable now + Assert.assertTrue(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + } + + /** + * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the + * KeysBackup object to be in the specified state + */ + fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + // If already in the wanted state, return + if (session.cryptoService().keysBackupService().state == state) { + return + } + + // Else observe state changes + val latch = CountDownLatch(1) + + session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + session.cryptoService().keysBackupService().removeListener(this) + latch.countDown() + } + } + }) + + mTestHelper.await(latch) + } + + fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { + Assert.assertNotNull(keys1) + Assert.assertNotNull(keys2) + + Assert.assertEquals(keys1?.algorithm, keys2?.algorithm) + Assert.assertEquals(keys1?.roomId, keys2?.roomId) + // No need to compare the shortcut + // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key) + Assert.assertEquals(keys1?.senderKey, keys2?.senderKey) + Assert.assertEquals(keys1?.sessionId, keys2?.sessionId) + Assert.assertEquals(keys1?.sessionKey, keys2?.sessionKey) + + assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain) + assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys) + } + + /** + * Common restore success check after [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]: + * - Imported keys number must be correct + * - The new device must have the same count of megolm keys + * - Alice must have the same keys on both devices + */ + fun checkRestoreSuccess(testData: KeysBackupScenarioData, + total: Int, + imported: Int) { + // - Imported keys number must be correct + Assert.assertEquals(testData.aliceKeys.size, total) + Assert.assertEquals(total, imported) + + // - The new device must have the same count of megolm keys + Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + + // - Alice must have the same keys on both devices + for (aliceKey1 in testData.aliceKeys) { + val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store + .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!) + Assert.assertNotNull(aliceKey2) + assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..c28b7990e0a6139c680169edf49b05fb45c63ec7 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo + +data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo, + val version: String) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..90d2fd781217e8c86e185c08a7505b7f543f2ba6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import java.util.concurrent.CountDownLatch + +/** + * This class observe the state change of a KeysBackup object and provide a method to check the several state change + * It checks all state transitions and detected forbidden transition + */ +internal class StateObserver(private val keysBackup: KeysBackupService, + private val latch: CountDownLatch? = null, + private val expectedStateChange: Int = -1) : KeysBackupStateListener { + + private val allowedStateTransitions = listOf( + KeysBackupState.BackingUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.BackingUp to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Disabled, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.NotTrusted, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.ReadyToBackUp, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Unknown, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.Disabled to KeysBackupState.Enabling, + + KeysBackupState.Enabling to KeysBackupState.Disabled, + KeysBackupState.Enabling to KeysBackupState.ReadyToBackUp, + + KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver, + // This transition happens when we trust the device + KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp, + + KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp, + + KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver, + + KeysBackupState.WillBackUp to KeysBackupState.BackingUp, + + KeysBackupState.WrongBackUpVersion to KeysBackupState.CheckingBackUpOnHomeserver, + + // FIXME These transitions are observed during test, and I'm not sure they should occur. Don't have time to investigate now + KeysBackupState.ReadyToBackUp to KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.Unknown + ) + + private val stateList = ArrayList<KeysBackupState>() + private var lastTransitionError: String? = null + + init { + keysBackup.addListener(this) + } + + // TODO Make expectedStates mandatory to enforce test + fun stopAndCheckStates(expectedStates: List<KeysBackupState>?) { + keysBackup.removeListener(this) + + expectedStates?.let { + assertEquals(it.size, stateList.size) + + for (i in it.indices) { + assertEquals("The state $i is not correct. states: " + stateList.joinToString(separator = " "), it[i], stateList[i]) + } + } + + assertNull("states: " + stateList.joinToString(separator = " "), lastTransitionError) + } + + override fun onStateChange(newState: KeysBackupState) { + stateList.add(newState) + + // Check that state transition is valid + if (stateList.size >= 2 + && !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) { + // Forbidden transition detected + lastTransitionError = "Forbidden transition detected from " + stateList[stateList.size - 2] + " to " + newState + } + + if (expectedStateChange == stateList.size) { + latch?.countDown() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt new file mode 100644 index 0000000000000000000000000000000000000000..42cee7433478a060e0ec27b34719b7f5b33b6058 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.ssss + +import androidx.lifecycle.Observer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent +import org.matrix.android.sdk.api.session.securestorage.KeySigner +import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QuadSTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + + private val emptyKeySigner = object : KeySigner { + override fun sign(canonicalJson: String): Map<String, Map<String, String>>? { + return null + } + } + + @Test + fun test_Generate4SKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync<SsssKeyCreationInfo> { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + } + + // Assert Account data is updated + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") + } + val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> + if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + + mTestHelper.await(accountDataLock) + + assertNotNull("Key should be stored in account data", accountData) + val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) + assertNotNull("Key Content cannot be parsed", parsed) + assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_AES_HMAC_SHA2, parsed!!.algorithm) + assertEquals("Unexpected key name", "Test Key", parsed.name) + assertNull("Key was not generated from passphrase", parsed.passphrase) + + // Set as default key + quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {}) + + var defaultKeyAccountData: UserAccountDataEvent? = null + val defaultDataLock = CountDownLatch(1) + + val liveDefAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> + if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { + defaultKeyAccountData = t.getOrNull()!! + defaultDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveDefAccountData.observeForever(accountDefDataObserver) } + + mTestHelper.await(defaultDataLock) + + assertNotNull(defaultKeyAccountData?.content) + assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecret() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId = "My.Key" + val info = generatedSecret(aliceSession, keyId, true) + + val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) + + // Store a secret + val clearSecret = "42".toByteArray().toBase64NoPadding() + mTestHelper.doSync<Unit> { + aliceSession.sharedSecretStorageService.storeSecret( + "secret.of.life", + clearSecret, + listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key + it + ) + } + + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") + + val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> + assertNotNull("Element should be encrypted", encryptedContent) + assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId)) + + val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId)) + assertNotNull(secret?.ciphertext) + assertNotNull(secret?.mac) + assertNotNull(secret?.initializationVector) + + // Try to decrypt?? + + val decryptedSecret = mTestHelper.doSync<String> { + aliceSession.sharedSecretStorageService.getSecret( + "secret.of.life", + null, // default key + keySpec!!, + it + ) + } + + assertEquals("Secret mismatch", clearSecret, decryptedSecret) + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_SetDefaultLocalEcho() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync<SsssKeyCreationInfo> { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + } + + // Test that we don't need to wait for an account data sync to access directly the keyid from DB + mTestHelper.doSync<Unit> { + quadS.setDefaultKey(TEST_KEY_ID, it) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecretWithMultipleKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val key1Info = generatedSecret(aliceSession, keyId1, true) + val keyId2 = "Key2" + val key2Info = generatedSecret(aliceSession, keyId2, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync<Unit> { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ), + it + ) + } + + val accountDataEvent = aliceSession.getAccountDataEvent("my.secret") + val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> + + assertEquals("Content should contains two encryptions", 2, encryptedContent?.keys?.size ?: 0) + + assertNotNull(encryptedContent?.get(keyId1)) + assertNotNull(encryptedContent?.get(keyId2)) + + // Assert that can decrypt with both keys + mTestHelper.doSync<String> { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, + it + ) + } + + mTestHelper.doSync<String> { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_GetSecretWithBadPassphrase() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val passphrase = "The good pass phrase" + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync<Unit> { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))), + it + ) + } + + val decryptCountDownLatch = CountDownLatch(1) + var error = false + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + "A bad passphrase", + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + object : MatrixCallback<String> { + override fun onSuccess(data: String) { + decryptCountDownLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + error = true + decryptCountDownLatch.countDown() + } + } + ) + + mTestHelper.await(decryptCountDownLatch) + + error shouldBe true + + // Now try with correct key + mTestHelper.doSync<String> { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + session.getLiveAccountDataEvent(type) + } + val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> + if (t?.getOrNull()?.type == type) { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + mTestHelper.await(accountDataLock) + + assertNotNull("Account Data type:$type should be found", accountData) + + return accountData!! + } + + private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { + quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + + if (asDefault) { + mTestHelper.doSync<Unit> { + quadS.setDefaultKey(keyId, it) + } + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } + + private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { + quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null, + it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + if (asDefault) { + val setDefaultLatch = CountDownLatch(1) + quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) + mTestHelper.await(setDefaultLatch) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6beeb123cc606f76fac1872335d9beea499379b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -0,0 +1,629 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +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.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SASTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_aliceStartThenAliceCancel() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val bobTxCreatedLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + bobTxCreatedLatch.countDown() + } + } + bobVerificationService.addListener(bobListener) + + val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, + bobSession.myUserId, + bobSession.cryptoService().getMyDevice().deviceId, + null) + assertNotNull("Alice should have a started transaction", txID) + + val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) + assertNotNull("Alice should have a started transaction", aliceKeyTx) + + mTestHelper.await(bobTxCreatedLatch) + bobVerificationService.removeListener(bobListener) + + val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID) + + assertNotNull("Bob should have started verif transaction", bobKeyTx) + assertTrue(bobKeyTx is SASDefaultVerificationTransaction) + assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) + assertTrue(aliceKeyTx is SASDefaultVerificationTransaction) + assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) + + val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction? + val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction? + + assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state) + assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state) + + // Let's cancel from alice side + val cancelLatch = CountDownLatch(1) + + val bobListener2 = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.transactionId == txID) { + val immutableState = (tx as SASDefaultVerificationTransaction).state + if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) { + cancelLatch.countDown() + } + } + } + } + bobVerificationService.addListener(bobListener2) + + aliceSasTx.cancel(CancelCode.User) + mTestHelper.await(cancelLatch) + + assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled) + assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled) + + val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled + val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled + + assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe) + assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe) + + assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode) + assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.cancelCode) + + assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) + assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_protocols_must_include_curve25519() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val protocols = listOf("meh_dont_know") + val tid = "00000000" + + // Bob should receive a cancel + var cancelReason: CancelCode? = null + val cancelLatch = CountDownLatch(1) + + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.transactionId == tid && tx.state is VerificationTxState.Cancelled) { + cancelReason = (tx.state as VerificationTxState.Cancelled).cancelCode + cancelLatch.countDown() + } + } + } + bobSession.cryptoService().verificationService().addListener(bobListener) + + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + (tx as IncomingSasVerificationTransaction).performAccept() + } + } + } + aliceSession.cryptoService().verificationService().addListener(aliceListener) + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) + + mTestHelper.await(cancelLatch) + + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_macs_Must_include_hmac_sha256() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val mac = listOf("shaBit") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_short_code_include_decimal() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val codes = listOf("bin", "foo", "bar") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.cleanUp(mTestHelper) + } + + private fun fakeBobStart(bobSession: Session, + aliceUserID: String?, + aliceDevice: String?, + tid: String, + protocols: List<String> = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, + hashes: List<String> = SASDefaultVerificationTransaction.KNOWN_HASHES, + mac: List<String> = SASDefaultVerificationTransaction.KNOWN_MACS, + codes: List<String> = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) { + val startMessage = KeyVerificationStart( + fromDevice = bobSession.cryptoService().getMyDevice().deviceId, + method = VerificationMethod.SAS.toValue(), + transactionId = tid, + keyAgreementProtocols = protocols, + hashes = hashes, + messageAuthenticationCodes = mac, + shortAuthenticationStrings = codes + ) + + val contentMap = MXUsersDevicesMap<Any>() + contentMap.setObject(aliceUserID, aliceDevice, startMessage) + + // TODO val sendLatch = CountDownLatch(1) + // TODO bobSession.cryptoRestClient.sendToDevice( + // TODO EventType.KEY_VERIFICATION_START, + // TODO contentMap, + // TODO tid, + // TODO TestMatrixCallback<Void>(sendLatch) + // TODO ) + } + + // any two devices may only have at most one key verification in flight at a time. + // If a device has two verifications in progress with the same device, then it should cancel both verifications. + @Test + fun test_aliceStartTwoRequests() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + + val aliceCreatedLatch = CountDownLatch(2) + val aliceCancelledLatch = CountDownLatch(2) + val createdTx = mutableListOf<SASDefaultVerificationTransaction>() + val aliceListener = object : VerificationService.Listener { + override fun transactionCreated(tx: VerificationTransaction) { + createdTx.add(tx as SASDefaultVerificationTransaction) + aliceCreatedLatch.countDown() + } + + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as SASDefaultVerificationTransaction).state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { + aliceCancelledLatch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobUserId = bobSession!!.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + + mTestHelper.await(aliceCreatedLatch) + mTestHelper.await(aliceCancelledLatch) + + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Test that when alice starts a 'correct' request, bob agrees. + */ + @Test + fun test_aliceAndBobAgreement() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + var accepted: ValidVerificationInfoAccept? = null + var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null + + val aliceAcceptedLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}") + if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { + val at = tx as SASDefaultVerificationTransaction + accepted = at.accepted + startReq = at.startReq + aliceAcceptedLatch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}") + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + bobVerificationService.removeListener(this) + val at = tx as IncomingSasVerificationTransaction + at.performAccept() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceAcceptedLatch) + + assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) + + // check that agreement is valid + assertTrue("Agreed Protocol should be Valid", accepted != null) + assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol)) + assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash)) + assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode)) + + accepted!!.shortAuthenticationStrings.forEach { + assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) + } + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_aliceAndBobSASCode() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState + when (uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + aliceSASLatch.countDown() + } + else -> Unit + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + tx.performAccept() + } + else -> Unit + } + if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { + bobSASLatch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction + val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction + + assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), + bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_happyPath() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState + Log.v("TEST", "== aliceState ${uxState.name}") + when (uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + tx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + if (matchOnce) { + matchOnce = false + aliceSASLatch.countDown() + } + } + else -> Unit + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + var acceptOnce = true + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState + Log.v("TEST", "== bobState ${uxState.name}") + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + if (acceptOnce) { + acceptOnce = false + tx.performAccept() + } + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + matchOnce = false + tx.userHasVerifiedShortCode() + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + bobSASLatch.countDown() + } + else -> Unit + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + // Assert that devices are verified + val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) + + // latch wait a bit again + Thread.sleep(1000) + + assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) + assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_ConcurrentStart() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val req = aliceVerificationService.requestKeyVerificationInDMs( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + bobSession.myUserId, + cryptoTestData.roomId + ) + + var requestID : String? = null + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId + } + } + + Log.v("TEST", "== requestID is $requestID") + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prBobPOV = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID + } + } + + bobVerificationService.readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + requestID!! + ) + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null + } + } + + // Start concurrent! + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + bobSession.myUserId, + bobSession.sessionParams.deviceId!!, + null) + + bobVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + aliceSession.myUserId, + aliceSession.sessionParams.deviceId!!, + null) + + // we should reach SHOW SAS on both + var alicePovTx: SasVerificationTransaction? + var bobPovTx: SasVerificationTransaction? + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd5aa32d5917faa8517c7d59efd251f7b735522f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +fun hexToByteArray(hex: String): ByteArray { + // Remove all spaces + return hex.replace(" ", "") + .let { + if (it.length % 2 != 0) "0$it" else it + } + .let { + ByteArray(it.length / 2) + .apply { + for (i in this.indices) { + val index = i * 2 + val v = it.substring(index, index + 2).toInt(16) + this[i] = v.toByte() + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..54a0f7e77195a5dd994fffd68d1991ee6b3916d7 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldEqual +import org.amshove.kluent.shouldEqualTo +import org.amshove.kluent.shouldNotBeNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QrCodeTest : InstrumentedTest { + + private val qrCode1 = QrCodeData.VerifyingAnotherUser( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²Ãq\u0087á®\u0013à \u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÃ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÃ\u008BAÂ¥12345678" + + private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²Ãq\u0087á®\u0013à \u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÃ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÃ\u008BAÂ¥12345678" + + private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = "MaTransaction", + deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÃ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÃ\u008BAÂ¥\u0092Ñ0qCú²Ãq\u0087á®\u0013à \u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" + + private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) + + private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5") + + private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55") + + @Test + fun testEncoding1() { + qrCode1.toEncodedString() shouldEqual value1 + } + + @Test + fun testEncoding2() { + qrCode2.toEncodedString() shouldEqual value2 + } + + @Test + fun testEncoding3() { + qrCode3.toEncodedString() shouldEqual value3 + } + + @Test + fun testSymmetry1() { + qrCode1.toEncodedString().toQrCodeData() shouldEqual qrCode1 + } + + @Test + fun testSymmetry2() { + qrCode2.toEncodedString().toQrCodeData() shouldEqual qrCode2 + } + + @Test + fun testSymmetry3() { + qrCode3.toEncodedString().toQrCodeData() shouldEqual qrCode3 + } + + @Test + fun testCase1() { + val url = qrCode1.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 0 + + checkSizeAndTransaction(byteArray) + + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase2() { + val url = qrCode2.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 1 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase3() { + val url = qrCode3.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 2 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testLongTransactionId() { + // Size on two bytes (2_000 = 0x07D0) + val longTransactionId = "PatternId_".repeat(200) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + val result = qrCode.toEncodedString() + val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId") + + result shouldEqual expected + + // Reverse operation + expected.toQrCodeData() shouldEqual qrCode + } + + @Test + fun testAnyTransactionId() { + for (qty in 0 until 0x1FFF step 200) { + val longTransactionId = "a".repeat(qty) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + // Symmetric operation + qrCode.toEncodedString().toQrCodeData() shouldEqual qrCode + } + } + + // Error cases + @Test + fun testErrorHeader() { + value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorVersion() { + value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorSecretTooShort() { + value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoTransactionNoKeyNoSecret() { + // But keep transaction length + "MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoKeyNoSecret() { + "MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorTransactionLengthTooShort() { + // In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch + value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull() + } + + @Test + fun testErrorTransactionLengthTooBig() { + value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull() + } + + private fun compareArray(actual: ByteArray, expected: ByteArray) { + actual.size shouldEqual expected.size + + for (i in actual.indices) { + actual[i] shouldEqualTo expected[i] + } + } + + private fun checkHeader(byteArray: ByteArray) { + // MATRIX + byteArray[0] shouldEqualTo 'M'.toByte() + byteArray[1] shouldEqualTo 'A'.toByte() + byteArray[2] shouldEqualTo 'T'.toByte() + byteArray[3] shouldEqualTo 'R'.toByte() + byteArray[4] shouldEqualTo 'I'.toByte() + byteArray[5] shouldEqualTo 'X'.toByte() + + // Version + byteArray[6] shouldEqualTo 2 + } + + private fun checkSizeAndTransaction(byteArray: ByteArray) { + // Size + byteArray[8] shouldEqualTo 0 + byteArray[9] shouldEqualTo 13 + + // Transaction + byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldEqual "MaTransaction" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4032890723aca891238e97f4ea3454f51b15b009 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SharedSecretTest : InstrumentedTest { + + @Test + fun testSharedSecretLengthCase() { + repeat(100) { + generateSharedSecretV2().length shouldBe 11 + } + } + + @Test + fun testSharedDiffCase() { + val sharedSecret1 = generateSharedSecretV2() + val sharedSecret2 = generateSharedSecretV2() + + sharedSecret1 shouldNotBeEqualTo sharedSecret2 + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c003215eef1dce63f6e82ac5c4b1eb957ceb02b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +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.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class VerificationTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + data class ExpectedResult( + val sasIsSupported: Boolean = false, + val otherCanScanQrCode: Boolean = false, + val otherCanShowQrCode: Boolean = false + ) + + private val sas = listOf( + VerificationMethod.SAS + ) + + private val sasShow = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW + ) + + private val sasScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SCAN + ) + + private val sasShowScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW, + VerificationMethod.QR_CODE_SCAN + ) + + @Test + fun test_aliceAndBob_sas_sas() = doTest( + sas, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_show() = doTest( + sas, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_sas() = doTest( + sasShow, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_scan() = doTest( + sas, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_sas() = doTest( + sasScan, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_scan() = doTest( + sasScan, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_show() = doTest( + sasShow, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_scan() = doTest( + sasShow, + sasScan, + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true) + ) + + @Test + fun test_aliceAndBob_scan_show() = doTest( + sasScan, + sasShow, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true) + ) + + @Test + fun test_aliceAndBob_all_all() = doTest( + sasShowScan, + sasShowScan, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true) + ) + + // TODO Add tests without SAS + + private fun doTest(aliceSupportedMethods: List<VerificationMethod>, + bobSupportedMethods: List<VerificationMethod>, + expectedResultForAlice: ExpectedResult, + expectedResultForBob: ExpectedResult) { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + + mTestHelper.doSync<Unit> { callback -> + aliceSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + mTestHelper.doSync<Unit> { callback -> + bobSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() + + var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null + var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null + + val latch = CountDownLatch(2) + val aliceListener = object : VerificationService.Listener { + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 4: Alice receive the ready request + if (pr.isReady) { + aliceReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // Step 2: Bob accepts the verification request + bobVerificationService.readyPendingVerificationInDMs( + bobSupportedMethods, + aliceSession.myUserId, + cryptoTestData.roomId, + pr.transactionId!! + ) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 3: Bob is ready + if (pr.isReady) { + bobReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + // Step 1: Alice starts a verification request + aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) + mTestHelper.await(latch) + + aliceReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForAlice.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForAlice.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForAlice.otherCanScanQrCode + } + + bobReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForBob.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode + } + + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b85310d506513668706b58f4e9a1bad6d3e4f86 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild, + * we can add more tests to cover the edge cases. + * Some tests are suffixed with `_not_passing`, maybe one day we will fix them... + * Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the + * formatted body, which is quite useless. + * Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. + * See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class MarkdownParserTest : InstrumentedTest { + + /** + * Create the same parser than in the RoomModule + */ + private val markdownParser = MarkdownParser( + Parser.builder().build(), + HtmlRenderer.builder().build(), + TextContentRenderer.builder().build() + ) + + @Test + fun parseNoMarkdown() { + testIdentity("") + testIdentity("a") + testIdentity("1") + testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" + + "modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" + + "atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + } + + @Test + fun parseSpaces() { + testIdentity(" ") + testIdentity(" ") + testIdentity("\n") + } + + @Test + fun parseNewLines() { + testIdentity("line1\nline2") + testIdentity("line1\nline2\nline3") + } + + @Test + fun parseBold() { + testType( + name = "bold", + markdownPattern = "**", + htmlExpectedTag = "strong" + ) + } + + @Test + fun parseItalic() { + testType( + name = "italic", + markdownPattern = "*", + htmlExpectedTag = "em" + ) + } + + @Test + fun parseItalic2() { + // Riot-Web format + "_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>") + } + + /** + * Note: the test is not passing, it does not work on Riot-Web neither + */ + @Test + fun parseStrike_not_passing() { + testType( + name = "strike", + markdownPattern = "~~", + htmlExpectedTag = "del" + ) + } + + @Test + fun parseCode() { + testType( + name = "code", + markdownPattern = "`", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode2() { + testType( + name = "code", + markdownPattern = "``", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode3() { + testType( + name = "code", + markdownPattern = "```", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseUnorderedList() { + "- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") } + "- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") } + } + + @Test + fun parseOrderedList() { + "1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") } + "1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") } + } + + @Test + fun parseHorizontalLine() { + "---".let { markdownParser.parse(it) }.expect("***", "<hr />") + } + + @Test + fun parseH2AndContent() { + "a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>") + } + + @Test + fun parseQuote() { + "> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>") + } + + @Test + fun parseQuote_not_passing() { + "> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>") + } + + @Test + fun parseBoldItalic() { + "*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>") + "**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>") + } + + @Test + fun parseHead() { + "# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>") + "## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>") + "### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>") + "#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>") + "##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>") + "###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>") + } + + @Test + fun parseHeads() { + "# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>") + } + + @Test + fun parseBoldNewLines_not_passing() { + "**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2") + } + + @Test + fun parseLinks() { + "[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""") + } + + @Test + fun parseParagraph() { + "# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>") + } + + private fun testIdentity(text: String) { + markdownParser.parse(text).expect(text, null) + } + + private fun testType(name: String, + markdownPattern: String, + htmlExpectedTag: String, + plainTextPrefix: String = "", + plainTextSuffix: String = "") { + // Test simple case + "$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>") + + // Test twice the same tag + "$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>") + + val textBefore = "a" + val textAfter = "b" + + // With sticked text before + "$textBefore$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>") + + // With text before and space + "$textBefore $markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>") + + // With sticked text after + "$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter") + + // With space and text after + "$markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter") + + // With sticked text before and text after + "$textBefore$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter") + + // With text before and after, with spaces + "$textBefore $markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter") + } + + private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) { + assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..854d420a82a2b57d192cb15685c49482bf6e2d93 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class JsonCanonicalizerTest : InstrumentedTest { + + @Test + fun identityTest() { + listOf( + "{}", + """{"a":true}""", + """{"a":false}""", + """{"a":1}""", + """{"a":1.2}""", + """{"a":null}""", + """{"a":[]}""", + """{"a":["b":"c"]}""", + """{"a":["c":"b","d":"e"]}""", + """{"a":["d":"b","c":"e"]}""" + ).forEach { + assertEquals(it, + JsonCanonicalizer.canonicalize(it)) + } + } + + @Test + fun reorderTest() { + assertEquals("""{"a":true,"b":false}""", + JsonCanonicalizer.canonicalize("""{"b":false,"a":true}""")) + } + + @Test + fun realSampleTest() { + assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""", + JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}""")) + } + + @Test + fun doubleQuoteTest() { + assertEquals("{\"a\":\"\\\"\"}", + JsonCanonicalizer.canonicalize("{\"a\":\"\\\"\"}")) + } + + /* ========================================================================================== + * Test from https://matrix.org/docs/spec/appendices.html#examples + * ========================================================================================== */ + + @Test + fun matrixOrg001Test() { + assertEquals("""{}""", + JsonCanonicalizer.canonicalize("""{}""")) + } + + @Test + fun matrixOrg002Test() { + assertEquals("""{"one":1,"two":"Two"}""", + JsonCanonicalizer.canonicalize("""{ + "one": 1, + "two": "Two" +}""")) + } + + @Test + fun matrixOrg003Test() { + assertEquals("""{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize("""{ + "b": "2", + "a": "1" +}""")) + } + + @Test + fun matrixOrg004Test() { + assertEquals("""{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}""")) + } + + @Test + fun matrixOrg005Test() { + assertEquals("""{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""", + JsonCanonicalizer.canonicalize("""{ + "auth": { + "success": true, + "mxid": "@john.doe:example.com", + "profile": { + "display_name": "John Doe", + "three_pids": [ + { + "medium": "email", + "address": "john.doe@example.org" + }, + { + "medium": "msisdn", + "address": "123456789" + } + ] + } + } +}""")) + } + + @Test + fun matrixOrg006Test() { + assertEquals("""{"a":"日本語"}""", + JsonCanonicalizer.canonicalize("""{ + "a": "日本語" +}""")) + } + + @Test + fun matrixOrg007Test() { + assertEquals("""{"æ—¥":1,"本":2}""", + JsonCanonicalizer.canonicalize("""{ + "本": 2, + "æ—¥": 1 +}""")) + } + + @Test + fun matrixOrg008Test() { + assertEquals("""{"a":"æ—¥"}""", + JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}")) + } + + @Test + fun matrixOrg009Test() { + assertEquals("""{"a":null}""", + JsonCanonicalizer.canonicalize("""{ + "a": null +}""")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a2a1586864de3c91559bdbbfc4366877d9352c49 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.merge +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents +import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.createObject +import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class ChunkEntityTest : InstrumentedTest { + + private lateinit var monarchy: Monarchy + + @Before + fun setup() { + Realm.init(context()) + val testConfig = RealmConfiguration.Builder() + .inMemory() + .name("test-realm") + .modules(SessionRealmModule()) + .build() + monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() + } + + @Test + fun add_shouldAdd_whenNotAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.timelineEvents.size shouldEqual 1 + } + } + + @Test + fun add_shouldNotAdd_whenAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.timelineEvents.size shouldEqual 1 + } + } + + @Test + fun merge_shouldAddEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.timelineEvents.size shouldEqual 60 + } + } + + @Test + fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val eventsForChunk1 = createFakeListOfEvents(30) + val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) + chunk1.isLastForward = true + chunk2.isLastForward = false + chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS) + chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.timelineEvents.size shouldEqual 40 + chunk1.isLastForward.shouldBeTrue() + } + } + + @Test + fun merge_shouldPrevTokenMerged_whenMergingForwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val prevToken = "prev_token" + chunk1.prevToken = prevToken + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS) + chunk1.prevToken shouldEqual prevToken + } + } + + @Test + fun merge_shouldNextTokenMerged_whenMergingBackwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val nextToken = "next_token" + chunk1.nextToken = nextToken + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.nextToken shouldEqual nextToken + } + } + + private fun ChunkEntity.addAll(roomId: String, + events: List<Event>, + direction: PaginationDirection) { + events.forEach { event -> + val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + addTimelineEvent(roomId, fakeEvent, direction, emptyMap()) + } + } + + companion object { + private const val ROOM_ID = "roomId" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a133032b6273f0756e37853d232376cd5292114 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEventPersistor +import kotlin.random.Random + +internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { + + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { + val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) + val tokenChunkEvent = FakeTokenChunkEvent( + Random.nextLong(System.currentTimeMillis()).toString(), + Random.nextLong(System.currentTimeMillis()).toString(), + fakeEvents + ) + return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..06828ef3d1b107c06618218a8fe62018c84a1680 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEventPersistor +import javax.inject.Inject +import kotlin.random.Random + +internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { + + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) + val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) + return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..0301157d09795c5c936f4133bf493535e2792812 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEvent + +internal data class FakeTokenChunkEvent(override val start: String?, + override val end: String?, + override val events: List<Event> = emptyList(), + override val stateEvents: List<Event> = emptyList() +) : TokenChunkEvent diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6fe6752181f70eba6d96a569232cd16df167dbf --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import kotlin.random.Random + +object RoomDataHelper { + + private const val FAKE_TEST_SENDER = "@sender:test.org" + private val EVENT_FACTORIES = hashMapOf( + 0 to { createFakeMessageEvent() }, + 1 to { createFakeRoomMemberEvent() } + ) + + fun createFakeListOfEvents(size: Int = 10): List<Event> { + return (0 until size).mapNotNull { + val nextInt = Random.nextInt(EVENT_FACTORIES.size) + EVENT_FACTORIES[nextInt]?.invoke() + } + } + + fun createFakeEvent(type: String, + content: Content? = null, + prevContent: Content? = null, + sender: String = FAKE_TEST_SENDER, + stateKey: String = FAKE_TEST_SENDER + ): Event { + return Event( + type = type, + eventId = Random.nextLong().toString(), + content = content, + prevContent = prevContent, + senderId = sender, + stateKey = stateKey + ) + } + + fun createFakeMessageEvent(): Event { + val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.MESSAGE, message) + } + + fun createFakeRoomMemberEvent(): Event { + val roomMember = RoomMemberSummary(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c5e7f17f2c39edc2a28e5631135766cddd7bbb4 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +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.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineBackToPreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an + * even contained in a previous lastForward chunk, we will be able to go back to the live + */ + @Test + fun backToPreviousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + var roomCreationEventId: String? = null + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + roomCreationEventId = snapshot.lastOrNull()?.root?.eventId + // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val messageRoot = "First messages from Alice" + + // Alice sends 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + messageRoot, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. + snapshot.size == 10 + && snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first event (room creation event), so inside the previous last forward chunk + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) + snapshot.size == 4 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically + assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) + + bobTimeline.restartWithEventId(roomCreationEventId) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item, and 30 for the forward pagination + && snapshot.size == 38 + && snapshot.checkSendOrder(messageRoot, 30, 0) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..facb905b3569f743d089039cd213caba207931a3 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +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.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineForwardPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we click to permalink, we will be able to go back to the live + */ + @Test + fun forwardPaginationTest() { + val numberOfMessagesToSend = 90 + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + // Alice sends X messages + val message = "Message from Alice" + val sentMessages = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + message, + numberOfMessagesToSend) + + // Alice clear the cache + commonTestHelper.doSync<Unit> { + aliceSession.clearCache(it) + } + + // And restarts the sync + aliceSession.startSync(true) + + val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30)) + aliceTimeline.start() + + // Alice sees the 10 last message of the room, and can only navigate BACKWARD + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Ok, we have the 10 last messages of the initial sync + snapshot.size == 10 + && snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(message).orFalse() } + } + + // Open the timeline at last sent message + aliceTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Alice navigates to the first message of the room, which is not in its database. A GET /context is performed + // Then she can paginate BACKWARD and FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // The event is not in db, so it is fetch alone + snapshot.size == 1 + && snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith("Message from Alice").orFalse() } + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + aliceTimeline.restartWithEventId(sentMessages.last().eventId) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Alice paginates BACKWARD and FORWARD of 50 events each + // Then she can only navigate FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Alice can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + && snapshot.size == 6 + 1 + 50 + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + // We ask to load event backward and forward + aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Alice paginates once again FORWARD for 50 events + // All the timeline is retrieved, she cannot paginate anymore in both direction + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend + && snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) + } + + aliceTimeline.addListener(aliceEventsListener) + + // Ask for a forward pagination + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + // The timeline is fully loaded + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + aliceTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..28ce75c221539982706dd4fcdfc54dfe586c34bd --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +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.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelinePreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live + */ + @Test + fun previousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val firstMessage = "First messages from Alice" + // Alice sends 30 messages + val firstMessageFromAliceId = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + firstMessage, + 30) + .last() + .eventId + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(firstMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val secondMessage = "Second messages from Alice" + // Alice sends again 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + secondMessage, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(secondMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first message sent from Alice + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is not in db, so it is fetch + snapshot.size == 1 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, and paginate in both direction + bobTimeline.restartWithEventId(firstMessageFromAliceId) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Paginate in both direction + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + snapshot.size == 8 + 1 + 35 + } + + bobTimeline.addListener(eventsListener) + + // Paginate in both direction + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + // Ensure the chunk in the middle is included in the next pagination + bobTimeline.paginate(Timeline.Direction.FORWARDS, 35) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future, till the live + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item 60 message from Alice + && snapshot.size == 8 + 60 + && snapshot.checkSendOrder(secondMessage, 30, 0) + && snapshot.checkSendOrder(firstMessage, 30, 30) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0da49cdbb14078370839487487a4365b44a9a78 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.InstrumentedTest + +internal class TimelineTest : InstrumentedTest { + + companion object { + private const val ROOM_ID = "roomId" + } + + private lateinit var monarchy: Monarchy + +// @Before +// fun setup() { +// Timber.plant(Timber.DebugTree()) +// Realm.init(context()) +// val testConfiguration = RealmConfiguration.Builder().name("test-realm") +// .modules(SessionRealmModule()).build() +// +// Realm.deleteRealm(testConfiguration) +// monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() +// RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) +// } +// +// private fun createTimeline(initialEventId: String? = null): Timeline { +// val taskExecutor = TaskExecutor(testCoroutineDispatchers) +// val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) +// val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor) +// val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor) +// val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) +// val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) +// return DefaultTimeline( +// ROOM_ID, +// initialEventId, +// monarchy.realmConfiguration, +// taskExecutor, +// getContextOfEventTask, +// timelineEventFactory, +// paginationTask, +// null) +// } +// +// @Test +// fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() { +// val timeline = createTimeline() +// timeline.start() +// val paginationCount = 30 +// var initialLoad = 0 +// val latch = CountDownLatch(2) +// var timelineEvents: List<TimelineEvent> = emptyList() +// timeline.listener = object : Timeline.Listener { +// override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { +// if (snapshot.isNotEmpty()) { +// if (initialLoad == 0) { +// initialLoad = snapshot.size +// } +// timelineEvents = snapshot +// latch.countDown() +// timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount) +// } +// } +// } +// latch.await() +// timelineEvents.size shouldEqual initialLoad + paginationCount +// timeline.dispose() +// } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt new file mode 100644 index 0000000000000000000000000000000000000000..324a3c1062c9ffd3618feb1ab827110481920cae --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import timber.log.Timber + +object RealmDebugTools { + /** + * Log info about the crypto DB + */ + fun dumpCryptoDb(realmConfiguration: RealmConfiguration) { + Realm.getInstance(realmConfiguration).use { + Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}") + + val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) } + Timber.d("Realm encryption key : $key") + + // Check if we have data + Timber.e("Realm is empty: ${it.isEmpty}") + + Timber.d("Realm has CryptoMetadataEntity: ${it.where<CryptoMetadataEntity>().count()}") + Timber.d("Realm has CryptoRoomEntity: ${it.where<CryptoRoomEntity>().count()}") + Timber.d("Realm has DeviceInfoEntity: ${it.where<DeviceInfoEntity>().count()}") + Timber.d("Realm has KeysBackupDataEntity: ${it.where<KeysBackupDataEntity>().count()}") + Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where<OlmInboundGroupSessionEntity>().count()}") + Timber.d("Realm has OlmSessionEntity: ${it.where<OlmSessionEntity>().count()}") + Timber.d("Realm has UserEntity: ${it.where<UserEntity>().count()}") + Timber.d("Realm has KeyInfoEntity: ${it.where<KeyInfoEntity>().count()}") + Timber.d("Realm has CrossSigningInfoEntity: ${it.where<CrossSigningInfoEntity>().count()}") + Timber.d("Realm has TrustLevelEntity: ${it.where<TrustLevelEntity>().count()}") + Timber.d("Realm has GossipingEventEntity: ${it.where<GossipingEventEntity>().count()}") + Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where<IncomingGossipingRequestEntity>().count()}") + Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where<OutgoingGossipingRequestEntity>().count()}") + Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where<MyDeviceLastSeenInfoEntity>().count()}") + } + } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee2c6076cc43841715015ada335ba32e803de971 --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Jeff Gilfelt. + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import org.matrix.android.sdk.internal.di.MatrixScope +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import timber.log.Timber +import java.io.IOException +import java.nio.charset.Charset +import javax.inject.Inject + +/** + * An OkHttp interceptor that logs requests as curl shell commands. They can then + * be copied, pasted and executed inside a terminal environment. This might be + * useful for troubleshooting client/server API interaction during development, + * making it easy to isolate and share requests made by the app. <p> Warning: The + * logs generated by this interceptor have the potential to leak sensitive + * information. It should only be used in a controlled manner or in a + * non-production environment. + */ +@MatrixScope +internal class CurlLoggingInterceptor @Inject constructor() + : Interceptor { + + /** + * Set any additional curl command options (see 'curl --help'). + */ + var curlOptions: String? = null + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + var compressed = false + + var curlCmd = "curl" + curlOptions?.let { + curlCmd += " $it" + } + curlCmd += " -X " + request.method + + val requestBody = request.body + if (requestBody != null) { + if (requestBody.contentLength() > 100_000) { + Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})") + // Ensure the curl command will failed + curlCmd += "DATA IS TOO BIG" + } else { + val buffer = Buffer() + requestBody.writeTo(buffer) + var charset: Charset? = UTF8 + val contentType = requestBody.contentType() + if (contentType != null) { + charset = contentType.charset(UTF8) + } + // try to keep to a single line and use a subshell to preserve any line breaks + curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" + } + } + + val headers = request.headers + var i = 0 + val count = headers.size + while (i < count) { + val name = headers.name(i) + val value = headers.value(i) + if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, ignoreCase = true)) { + compressed = true + } + curlCmd += " -H \"$name: $value\"" + i++ + } + + curlCmd += ((if (compressed) " --compressed " else " ") + "'" + request.url.toString() + // Replace localhost for emulator by localhost for shell + .replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/") + + "'") + + // Add Json formatting + curlCmd += " | python -m json.tool" + + Timber.d("--- cURL (${request.url})") + Timber.d(curlCmd) + + return chain.proceed(request) + } + + companion object { + private val UTF8 = Charset.forName("UTF-8") + } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt new file mode 100644 index 0000000000000000000000000000000000000000..349110aff8a9acc515481dc95ceb52c12de310e3 --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import androidx.annotation.NonNull +import org.matrix.android.sdk.BuildConfig +import okhttp3.logging.HttpLoggingInterceptor +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber + +class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { + + companion object { + private const val INDENT_SPACE = 2 + } + + /** + * Log the message and try to log it again as a JSON formatted string + * Note: it can consume a lot of memory but it is only in DEBUG mode + * + * @param message + */ + @Synchronized + override fun log(@NonNull message: String) { + // In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG + if (BuildConfig.DEBUG) { + Timber.v(message) + + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) + } + } + // Else not a json string to log + } + } + + private fun logJson(formattedJson: String) { + val arr = formattedJson.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (s in arr) { + Timber.v(s) + } + } +} diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..52238f824cfaf94e6c52a7e1b0c5e4dd6b694718 --- /dev/null +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.matrix.android.sdk"> + + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + + <application android:networkSecurityConfig="@xml/network_security_config"> + + <!-- + The SDK offers a secured File provider to access downloaded files. + Access to these file will be given via the FileService, with a temporary + read access permission + --> + <provider + android:name=".api.session.file.MatrixSDKFileProvider" + android:authorities="${applicationId}.mx-sdk.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/sdk_provider_paths" /> + </provider> + </application> + +</manifest> diff --git a/matrix-sdk-android/src/main/assets/postMessageAPI.js b/matrix-sdk-android/src/main/assets/postMessageAPI.js new file mode 100755 index 0000000000000000000000000000000000000000..4936a7853882cb3366b747d2bde6b9ba7be77845 --- /dev/null +++ b/matrix-sdk-android/src/main/assets/postMessageAPI.js @@ -0,0 +1,54 @@ +var android_widget_events = {}; + +var sendObjectMessageToRiotAndroid = function(parameters) { + Android.onWidgetEvent(JSON.stringify(parameters)); +}; + +var onWidgetMessageToRiotAndroid = function(event) { + /* Use an internal "_id" field for matching onMessage events and requests + _id was originally used by the Modular API. Keep it */ + if (!event.data._id) { + /* The Matrix Widget API v2 spec says: + "The requestId field should be unique and included in all requests" */ + event.data._id = event.data.requestId; + } + /* Make sure to have one id */ + if (!event.data._id) { + event.data._id = Date.now() + "-" + Math.random().toString(36); + } + + console.log("onWidgetMessageToRiotAndroid " + event.data._id); + + if (android_widget_events[event.data._id]) { + console.log("onWidgetMessageToRiotAndroid : already managed"); + return; + } + + if (!event.origin) { + event.origin = event.originalEvent.origin; + } + + android_widget_events[event.data._id] = event; + + console.log("onWidgetMessageToRiotAndroid : manage " + event.data); + sendObjectMessageToRiotAndroid({'event.data': event.data}); +}; + +var sendResponseFromRiotAndroid = function(eventId, res) { + var event = android_widget_events[eventId]; + + console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res)); + + var data = JSON.parse(JSON.stringify(event.data)); + + data.response = res; + + console.log("sendResponseFromRiotAndroid ---> " + data); + + event.source.postMessage(data, event.origin); + android_widget_events[eventId] = true; + + console.log("sendResponseFromRiotAndroid to done"); +}; + +window.addEventListener('message', onWidgetMessageToRiotAndroid, false); diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt new file mode 100644 index 0000000000000000000000000000000000000000..6cd003ddae4865952d50c8b7245e9f0d5d77b91e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.di.DaggerMatrixComponent +import org.matrix.android.sdk.internal.network.UserAgentHolder +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.olm.OlmManager +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +/** + * This is the main entry point to the matrix sdk. + * To get the singleton instance, use getInstance static method. + */ +class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + + @Inject internal lateinit var legacySessionImporter: LegacySessionImporter + @Inject internal lateinit var authenticationService: AuthenticationService + @Inject internal lateinit var userAgentHolder: UserAgentHolder + @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + @Inject internal lateinit var olmManager: OlmManager + @Inject internal lateinit var sessionManager: SessionManager + + init { + Monarchy.init(context) + DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this) + if (context.applicationContext !is Configuration.Provider) { + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } + + fun getUserAgent() = userAgentHolder.userAgent + + fun authenticationService(): AuthenticationService { + return authenticationService + } + + fun legacySessionImporter(): LegacySessionImporter { + return legacySessionImporter + } + + companion object { + + private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) + + fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) { + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext, matrixConfiguration) + } + } + + fun getInstance(context: Context): Matrix { + if (isInit.compareAndSet(false, true)) { + val appContext = context.applicationContext + if (appContext is MatrixConfiguration.Provider) { + val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() + instance = Matrix(appContext, matrixConfiguration) + } else { + throw IllegalStateException("Matrix is not initialized properly." + + " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.") + } + } + return instance + } + + fun getSdkVersion(): String { + return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + } + + fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..e20d9074a8d9845dd1413065d9d7686ffa560fa7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +/** + * Generic callback interface for asynchronously. + * @param <T> the type of data to return on success + */ +interface MatrixCallback<in T> { + + /** + * On success method, default to no-op + * @param data the data successfully returned from the async function + */ + fun onSuccess(data: T) { + // no-op + } + + /** + * On failure method, default to no-op + * @param failure the failure data returned from the async function + */ + fun onFailure(failure: Throwable) { + // no-op + } +} + +/** + * Basic no op implementation + */ +class NoOpMatrixCallback<T>: MatrixCallback<T> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt new file mode 100644 index 0000000000000000000000000000000000000000..bfcc9105ebf9d5e9c45a25c018526fbc84731871 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import java.net.Proxy + +data class MatrixConfiguration( + val applicationFlavor: String = "Default-application-flavor", + val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), + val integrationUIUrl: String = "https://scalar.vector.im/", + val integrationRestUrl: String = "https://scalar.vector.im/api", + val integrationWidgetUrls: List<String> = listOf( + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ), + /** + * Optional proxy to connect to the matrix servers + * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) + */ + val proxy: Proxy? = null +) { + + /** + * Can be implemented by your Application class + */ + interface Provider { + fun providesMatrixConfiguration(): MatrixConfiguration + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt new file mode 100644 index 0000000000000000000000000000000000000000..f6e9a33aeeff378c6b8ab37bbdbafae158874420 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +/** + * This class contains pattern to match the different Matrix ids + */ +object MatrixPatterns { + + // Note: TLD is not mandatory (localhost, IP address...) + private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?" + + // regex pattern to find matrix user ids in a string. + // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids + private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" + val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find room ids in a string. + private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find room aliases in a string. + private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find message ids in a string. + private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find message ids in a string. + private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // Ref: https://matrix.org/docs/spec/rooms/v4#event-ids + private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find group ids in a string. + private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find permalink with message id. + // Android does not support in URL so extract it. + private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" + private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" + const val SEP_REGEX = "/" + + private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + // list of patterns to find some matrix item. + val MATRIX_PATTERNS = listOf( + PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID, + PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS, + PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID, + PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS, + PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_ALIAS, + PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER + ) + + /** + * Tells if a string is a valid user Id. + * + * @param str the string to test + * @return true if the string is a valid user id + */ + fun isUserId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER + } + + /** + * Tells if a string is a valid room id. + * + * @param str the string to test + * @return true if the string is a valid room Id + */ + fun isRoomId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER + } + + /** + * Tells if a string is a valid room alias. + * + * @param str the string to test + * @return true if the string is a valid room alias. + */ + fun isRoomAlias(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_ALIAS + } + + /** + * Tells if a string is a valid event id. + * + * @param str the string to test + * @return true if the string is a valid event id. + */ + fun isEventId(str: String?): Boolean { + return str != null + && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + } + + /** + * Tells if a string is a valid group id. + * + * @param str the string to test + * @return true if the string is a valid group id. + */ + fun isGroupId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER + } + + /** + * Extract server name from a matrix id + * + * @param matrixId + * @return null if not found or if matrixId is null + */ + fun extractServerNameFromId(matrixId: String?): String? { + return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..91e2845cd2739bf98a998348f4aff745e2f2a8c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +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.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to authenticate or to create an account to a matrix server. + */ +interface AuthenticationService { + /** + * Request the supported login flows for this homeserver. + * This is the first method to call to be able to get a wizard to login or the create an account + */ + fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable + + /** + * Request the supported login flows for the corresponding sessionId. + */ + fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable + + /** + * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. + */ + fun getLoginWizard(): LoginWizard + + /** + * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first. + */ + fun getRegistrationWizard(): RegistrationWizard + + /** + * True when login and password has been sent with success to the homeserver + */ + val isRegistrationStarted: Boolean + + /** + * Cancel pending login or pending registration + */ + fun cancelPendingLoginOrRegistration() + + /** + * Reset all pending settings, including current HomeServerConnectionConfig + */ + fun reset() + + /** + * Check if there is an authenticated [Session]. + * @return true if there is at least one active session. + */ + fun hasAuthenticatedSessions(): Boolean + + /** + * Get the last authenticated [Session], if there is an active session. + * @return the last active session if any, or null + */ + fun getLastAuthenticatedSession(): Session? + + /** + * Create a session after a SSO successful login + */ + fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, + credentials: Credentials, + callback: MatrixCallback<Session>): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, + callback: MatrixCallback<WellknownResult>): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback<Session>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt new file mode 100644 index 0000000000000000000000000000000000000000..590b84f35b0798928b613856dcdd9ac300b20064 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth + +/** + * Path to use when the client does not supported any or all login flows + * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback + * */ +const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" + +/** + * Path to use when the client does not supported any or all registration flows + * Not documented + */ +const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" + +/** + * Path to use when the client want to connect using SSO + * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login + */ +const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" + +const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt new file mode 100644 index 0000000000000000000000000000000000000000..6dfa56f16a19f0a30429f4cca3165b9f82159197 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.util.md5 + +/** + * This data class hold credentials user data. + * You shouldn't have to instantiate it. + * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ + @Json(name = "user_id") val userId: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ + @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ + @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String?, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ + @Json(name = "device_id") val deviceId: String?, + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null +) + +internal fun Credentials.sessionId(): String { + return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000000000000000000000000000000000..d5d732ccc2c7d8fdb11fbd912b94df8d84b5e59e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..02fab04067277c83b32db189a0a7a446bd7c0b7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import android.net.Uri +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig.Builder +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.CipherSuite +import okhttp3.TlsVersion + +/** + * This data class holds how to connect to a specific Homeserver. + * It's used with [org.matrix.android.sdk.api.auth.AuthenticationService] class. + * You should use the [Builder] to create one. + */ +@JsonClass(generateAdapter = true) +data class HomeServerConnectionConfig( + val homeServerUri: Uri, + val identityServerUri: Uri? = null, + val antiVirusServerUri: Uri? = null, + val allowedFingerprints: List<Fingerprint> = emptyList(), + val shouldPin: Boolean = false, + val tlsVersions: List<TlsVersion>? = null, + val tlsCipherSuites: List<CipherSuite>? = null, + val shouldAcceptTlsExtensions: Boolean = true, + val allowHttpExtension: Boolean = false, + val forceUsageTlsVersions: Boolean = false +) { + + /** + * This builder should be use to create a [HomeServerConnectionConfig] instance. + */ + class Builder { + + private lateinit var homeServerUri: Uri + private var identityServerUri: Uri? = null + private var antiVirusServerUri: Uri? = null + private val allowedFingerprints: MutableList<Fingerprint> = ArrayList() + private var shouldPin: Boolean = false + private val tlsVersions: MutableList<TlsVersion> = ArrayList() + private val tlsCipherSuites: MutableList<CipherSuite> = ArrayList() + private var shouldAcceptTlsExtensions: Boolean = true + private var allowHttpExtension: Boolean = false + private var forceUsageTlsVersions: Boolean = false + + fun withHomeServerUri(hsUriString: String): Builder { + return withHomeServerUri(Uri.parse(hsUriString)) + } + + /** + * @param hsUri The URI to use to connect to the homeserver. + * @return this builder + */ + fun withHomeServerUri(hsUri: Uri): Builder { + if (hsUri.scheme != "http" && hsUri.scheme != "https") { + throw RuntimeException("Invalid home server URI: $hsUri") + } + // ensure trailing / + val hsString = hsUri.toString().ensureTrailingSlash() + homeServerUri = try { + Uri.parse(hsString) + } catch (e: Exception) { + throw RuntimeException("Invalid home server URI: $hsUri") + } + return this + } + + fun withIdentityServerUri(identityServerUriString: String): Builder { + return withIdentityServerUri(Uri.parse(identityServerUriString)) + } + + /** + * @param identityServerUri The URI to use to manage identity. + * @return this builder + */ + fun withIdentityServerUri(identityServerUri: Uri): Builder { + if (identityServerUri.scheme != "http" && identityServerUri.scheme != "https") { + throw RuntimeException("Invalid identity server URI: $identityServerUri") + } + // ensure trailing / + val isString = identityServerUri.toString().ensureTrailingSlash() + this.identityServerUri = try { + Uri.parse(isString) + } catch (e: Exception) { + throw RuntimeException("Invalid identity server URI: $identityServerUri") + } + return this + } + + /** + * @param allowedFingerprints If using SSL, allow server certs that match these fingerprints. + * @return this builder + */ + fun withAllowedFingerPrints(allowedFingerprints: List<Fingerprint>?): Builder { + if (allowedFingerprints != null) { + this.allowedFingerprints.addAll(allowedFingerprints) + } + return this + } + + /** + * @param pin If true only allow certs matching given fingerprints, otherwise fallback to + * standard X509 checks. + * @return this builder + */ + fun withPin(pin: Boolean): Builder { + this.shouldPin = pin + return this + } + + /** + * @param shouldAcceptTlsExtension + * @return this builder + */ + fun withShouldAcceptTlsExtensions(shouldAcceptTlsExtension: Boolean): Builder { + this.shouldAcceptTlsExtensions = shouldAcceptTlsExtension + return this + } + + /** + * Add an accepted TLS version for TLS connections with the home server. + * + * @param tlsVersion the tls version to add to the set of TLS versions accepted. + * @return this builder + */ + fun addAcceptedTlsVersion(tlsVersion: TlsVersion): Builder { + this.tlsVersions.add(tlsVersion) + return this + } + + /** + * Force the usage of TlsVersion. This can be usefull for device on Android version < 20 + * + * @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with [.addAcceptedTlsVersion] + * @return this builder + */ + fun forceUsageOfTlsVersions(forceUsageOfTlsVersions: Boolean): Builder { + this.forceUsageTlsVersions = forceUsageOfTlsVersions + return this + } + + /** + * Add a TLS cipher suite to the list of accepted TLS connections with the home server. + * + * @param tlsCipherSuite the tls cipher suite to add. + * @return this builder + */ + fun addAcceptedTlsCipherSuite(tlsCipherSuite: CipherSuite): Builder { + this.tlsCipherSuites.add(tlsCipherSuite) + return this + } + + fun withAntiVirusServerUri(antivirusServerUriString: String?): Builder { + return withAntiVirusServerUri(antivirusServerUriString?.let { Uri.parse(it) }) + } + + /** + * Update the anti-virus server URI. + * + * @param antivirusServerUri the new anti-virus uri. Can be null + * @return this builder + */ + fun withAntiVirusServerUri(antivirusServerUri: Uri?): Builder { + if (null != antivirusServerUri && "http" != antivirusServerUri.scheme && "https" != antivirusServerUri.scheme) { + throw RuntimeException("Invalid antivirus server URI: $antivirusServerUri") + } + this.antiVirusServerUri = antivirusServerUri + return this + } + + /** + * Convenient method to limit the TLS versions and cipher suites for this Builder + * Ref: + * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://developer.android.com/reference/javax/net/ssl/SSLEngine + * + * @param tlsLimitations true to use Tls limitations + * @param enableCompatibilityMode set to true for Android < 20 + * @return this builder + */ + fun withTlsLimitations(tlsLimitations: Boolean, enableCompatibilityMode: Boolean): Builder { + if (tlsLimitations) { + withShouldAcceptTlsExtensions(false) + + // Tls versions + addAcceptedTlsVersion(TlsVersion.TLS_1_2) + addAcceptedTlsVersion(TlsVersion.TLS_1_3) + + forceUsageOfTlsVersions(enableCompatibilityMode) + + // Cipher suites + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) + + if (enableCompatibilityMode) { + // Adopt some preceding cipher suites for Android < 20 to be able to negotiate + // a TLS session. + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA) + } + } + return this + } + + fun withAllowHttpConnection(allowHttpExtension: Boolean): Builder { + this.allowHttpExtension = allowHttpExtension + return this + } + + /** + * @return the [HomeServerConnectionConfig] + */ + fun build(): HomeServerConnectionConfig { + return HomeServerConnectionConfig( + homeServerUri, + identityServerUri, + antiVirusServerUri, + allowedFingerprints, + shouldPin, + tlsVersions, + tlsCipherSuites, + shouldAcceptTlsExtensions, + allowHttpExtension, + forceUsageTlsVersions + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3686da7dda96047cbe0d60b51a88c0582f4f79c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +// Either a list of supported login types, or an error if the homeserver is outdated +sealed class LoginFlowResult { + data class Success( + val supportedLoginTypes: List<String>, + val isLoginAndRegistrationSupported: Boolean, + val homeServerUrl: String + ) : LoginFlowResult() + + object OutdatedHomeserver : LoginFlowResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt new file mode 100644 index 0000000000000000000000000000000000000000..64a1fd88d178024541f8e1faa5f890b478b78dcd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +object LoginFlowTypes { + const val PASSWORD = "m.login.password" + const val OAUTH2 = "m.login.oauth2" + const val EMAIL_CODE = "m.login.email.code" + const val EMAIL_URL = "m.login.email.url" + const val EMAIL_IDENTITY = "m.login.email.identity" + const val MSISDN = "m.login.msisdn" + const val RECAPTCHA = "m.login.recaptcha" + const val DUMMY = "m.login.dummy" + const val TERMS = "m.login.terms" + const val TOKEN = "m.login.token" + const val SSO = "m.login.sso" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..cbeece7e037c4c83185a0b80c1bfa5ae6b5caf81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +/** + * This data class holds necessary data to open a session. + * You don't have to manually instantiate it. + */ +data class SessionParams( + /** + * Please consider using shortcuts instead + */ + val credentials: Credentials, + + /** + * Please consider using shortcuts instead + */ + val homeServerConnectionConfig: HomeServerConnectionConfig, + + /** + * Set to false if the current token is not valid anymore. Application should not have to use this info. + */ + val isTokenValid: Boolean +) { + /* + * Shortcuts. Usually the application should only need to use these shortcuts + */ + + /** + * The userId of the session (Ex: "@user:domain.org") + */ + val userId = credentials.userId + + /** + * The deviceId of the session (Ex: "ABCDEFGH") + */ + val deviceId = credentials.deviceId + + /** + * The current homeserver Url. It can be different that the homeserver url entered + * during login phase, because a redirection may have occurred + */ + val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString() + + /** + * The current homeserver host + */ + val homeServerHost = homeServerConnectionConfig.homeServerUri.host + + /** + * The default identity server url if any, returned by the homeserver during login phase + */ + val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4bd8badd754de6c22024f183072ad1a6a6f559e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * <pre> + * { + * "m.homeserver": { + * "base_url": "https://matrix.org" + * }, + * "m.identity_server": { + * "base_url": "https://vector.im" + * } + * "m.integrations": { + * "managers": [ + * { + * "api_url": "https://integrations.example.org", + * "ui_url": "https://integrations.example.org/ui" + * }, + * { + * "api_url": "https://bots.example.org" + * } + * ] + * } + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +data class WellKnown( + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null, + + @Json(name = "m.integrations") + val integrations: JsonDict? = null, + + @Json(name = "im.vector.riot.e2ee") + val e2eAdminSetting: E2EWellKnownConfig? = null + +) + +@JsonClass(generateAdapter = true) +data class E2EWellKnownConfig( + @Json(name = "default") + val e2eDefault: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0b252f973582a3ac4f79779063316b3662b1ff4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * <pre> + * { + * "base_url": "https://vector.im" + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +data class WellKnownBaseConfig( + @Json(name = "base_url") + val baseURL: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt new file mode 100644 index 0000000000000000000000000000000000000000..25cf8209fed5ee2dba12e02bf06926c960002c79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.login + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable + +interface LoginWizard { + + /** + * @param login the login field + * @param password the password field + * @param deviceName the initial device name + * @param callback the matrix callback on which you'll receive the result of authentication. + * @return return a [Cancelable] + */ + fun login(login: String, + password: String, + deviceName: String, + callback: MatrixCallback<Session>): Cancelable + + /** + * Exchange a login token to an access token + */ + fun loginWithToken(loginToken: String, + callback: MatrixCallback<Session>): Cancelable + + /** + * Reset user password + */ + fun resetPassword(email: String, + newPassword: String, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Confirm the new password, once the user has checked his email + */ + fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt new file mode 100644 index 0000000000000000000000000000000000000000..3dd2b460b2c25b3fdac9c342c378ef7444641361 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.registration + +sealed class RegisterThreePid { + data class Email(val email: String) : RegisterThreePid() + data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..544cbf63ccb0478e554e5de04f5b01bcc35e8138 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.registration + +import org.matrix.android.sdk.api.session.Session + +// Either a session or an object containing data about registration stages +sealed class RegistrationResult { + data class Success(val session: Session) : RegistrationResult() + data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() +} + +data class FlowResult( + val missingStages: List<Stage>, + val completedStages: List<Stage> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt new file mode 100644 index 0000000000000000000000000000000000000000..0629915a426a50345a5507f226ecfde626d1aad5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.registration + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface RegistrationWizard { + + fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable + + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback<RegistrationResult>): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable + + fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable + + fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable + + fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable + + fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable + + fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable + + fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable + + val currentThreePid: String? + + // True when login and password has been sent with success to the homeserver + val isRegistrationStarted: Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt new file mode 100644 index 0000000000000000000000000000000000000000..2635adc7339e124b090e4665d15462138a3ac3b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.registration + +sealed class Stage(open val mandatory: Boolean) { + + // m.login.recaptcha + data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.email.identity + data class Email(override val mandatory: Boolean) : Stage(mandatory) + + // m.login.msisdn + data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) + + // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username + // and a password, the dummy stage has to be done + data class Dummy(override val mandatory: Boolean) : Stage(mandatory) + + // Undocumented yet: m.login.terms + data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // For unknown stages + data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) +} + +typealias TermPolicies = Map<*, *> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..a736a4f1be8ffe5afc86417856f12c8e01d1adf8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.wellknown + +import org.matrix.android.sdk.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt new file mode 100644 index 0000000000000000000000000000000000000000..409fec4437687032eacc0aee8374a3747d471352 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.comparators + +import org.matrix.android.sdk.api.interfaces.DatedObject + +object DatedObjectComparators { + + /** + * Comparator to sort DatedObjects from the oldest to the latest. + */ + val ascComparator by lazy { + Comparator<DatedObject> { datedObject1, datedObject2 -> + (datedObject1.date - datedObject2.date).toInt() + } + } + + /** + * Comparator to sort DatedObjects from the latest to the oldest. + */ + val descComparator by lazy { + Comparator<DatedObject> { datedObject1, datedObject2 -> + (datedObject2.date - datedObject1.date).toInt() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt new file mode 100644 index 0000000000000000000000000000000000000000..97454684a3e50a46f256ae3ea3daee1dbd72cde8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.crypto + +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.internal.crypto.verification.getEmojiForCode + +/** + * Provide all the emojis used for SAS verification (for debug purpose) + */ +fun getAllVerificationEmojis(): List<EmojiRepresentation> { + return (0..63).map { getEmojiForCode(it) } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..9eae1265f0e8e5d85a431f7867d279ec7f156ef7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.crypto + +/** + * Class to define the parameters used to customize or configure the end-to-end crypto. + */ +data class MXCryptoConfig constructor( + // Tell whether the encryption of the event content is enabled for the invited members. + // SDK clients can disable this by settings it to false. + // Note that the encryption for the invited members will be blocked if the history visibility is "joined". + val enableEncryptionForInvitedMembers: Boolean = true, + + /** + * If set to true, the SDK will automatically ignore room key request (gossiping) + * coming from your other untrusted sessions (or blocked). + * If set to false, the request will be forwarded to the application layer; in this + * case the application can decide to prompt the user. + */ + val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt new file mode 100644 index 0000000000000000000000000000000000000000..23f21f88293eaff6d37b6a7b99e000a1b3c0930e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.crypto + +/** + * RoomEncryptionTrustLevel represents the trust level in an encrypted room. + */ +enum class RoomEncryptionTrustLevel { + // No one in the room has been verified -> Black shield + Default, + + // There are one or more device un-verified -> the app should display a red shield + Warning, + + // All devices in the room are verified -> the app should display a green shield + Trusted +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt new file mode 100644 index 0000000000000000000000000000000000000000..606f3211965e76da7bbf0864e822270865de0e3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.extensions + +fun Boolean?.orTrue() = this ?: true + +fun Boolean?.orFalse() = this ?: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f3c8c13c5553a5b5c22fa6248c9241051c2c7ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.extensions + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo + +/* ========================================================================================== + * MXDeviceInfo + * ========================================================================================== */ + +fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint() + ?.chunked(4) + ?.joinToString(separator = " ") + +/* ========================================================================================== + * DeviceInfo + * ========================================================================================== */ + +fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> { + return this.sortedByDescending { it.lastSeenTs ?: 0 } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt new file mode 100644 index 0000000000000000000000000000000000000000..f25898077aa92a9d1cfa6d086632c508bc1f60fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.extensions + +fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { + return when { + startsWith(prefix) -> this + else -> "$prefix$this" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt new file mode 100644 index 0000000000000000000000000000000000000000..baae9b70f582fb20e4f2454659dd2ee4e2fb7e88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.extensions + +import timber.log.Timber + +inline fun <A> tryThis(message: String? = null, operation: () -> A): A? { + return try { + operation() + } catch (any: Throwable) { + if (message != null) { + Timber.e(any, message) + } + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8caed519b2cbf66fa05e8daee71903545bf27e27 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.di.MoshiProvider +import java.io.IOException +import javax.net.ssl.HttpsURLConnection + +fun Throwable.is401() = + this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_UNAUTHORIZED + +fun Throwable.isTokenError() = + this is Failure.ServerError + && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) + +fun Throwable.shouldBeRetried(): Boolean { + return this is Failure.NetworkConnection + || this is IOException + || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) +} + +fun Throwable.isInvalidPassword(): Boolean { + return this is Failure.ServerError + && error.code == MatrixError.M_FORBIDDEN + && error.message == "Invalid password" +} + +/** + * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible + */ +fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { + return if (this is Failure.OtherServerError && this.httpCode == 401) { + tryThis { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(this.errorBody) + } + } else { + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt new file mode 100644 index 0000000000000000000000000000000000000000..a930d7d633de54f996425bc9689c8fdc68a14b03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import java.io.IOException + +/** + * This class allows to expose different kinds of error to be then handled by the application. + * As it is a sealed class, you typically use it like that : + * when(failure) { + * is NetworkConnection -> Unit + * is ServerError -> Unit + * is Unknown -> Unit + * } + */ +sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { + data class Unknown(val throwable: Throwable? = null) : Failure(throwable) + data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) + data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure() + data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) + data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) + object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) + + // When server send an error, but it cannot be interpreted as a MatrixError + data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) + + data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) + + data class CryptoError(val error: MXCryptoError) : Failure(error) + + abstract class FeatureFailure : Failure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt new file mode 100644 index 0000000000000000000000000000000000000000..053ad670b91c90901bd58da840cbcb5ada91d797 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.internal.network.ssl.Fingerprint + +// This class will be sent to the bus +sealed class GlobalError { + data class InvalidToken(val softLogout: Boolean) : GlobalError() + data class ConsentNotGivenError(val consentUri: String) : GlobalError() + data class CertificateError(val fingerprint: Fingerprint) : GlobalError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff68107ffcb585dac73fe5bd9a8c9269c1a6dbb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.failure + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This data class holds the error defined by the matrix specifications. + * You shouldn't have to instantiate it. + * Ref: https://matrix.org/docs/spec/client_server/latest#api-standards + */ +@JsonClass(generateAdapter = true) +data class MatrixError( + /** unique string which can be used to handle an error message */ + @Json(name = "errcode") val code: String, + /** human-readable error message */ + @Json(name = "error") val message: String, + + // For M_CONSENT_NOT_GIVEN + @Json(name = "consent_uri") val consentUri: String? = null, + // For M_RESOURCE_LIMIT_EXCEEDED + @Json(name = "limit_type") val limitType: String? = null, + @Json(name = "admin_contact") val adminUri: String? = null, + // For M_LIMIT_EXCEEDED + @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, + // For M_UNKNOWN_TOKEN + @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + // For M_INVALID_PEPPER + // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} + @Json(name = "lookup_pepper") val newLookupPepper: String? = null +) { + + companion object { + /** Forbidden access, e.g. joining a room without permission, failed login. */ + const val M_FORBIDDEN = "M_FORBIDDEN" + /** An unknown error has occurred. */ + const val M_UNKNOWN = "M_UNKNOWN" + /** The access token specified was not recognised. */ + const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" + /** No access token was specified for the request. */ + const val M_MISSING_TOKEN = "M_MISSING_TOKEN" + /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */ + const val M_BAD_JSON = "M_BAD_JSON" + /** Request did not contain valid JSON. */ + const val M_NOT_JSON = "M_NOT_JSON" + /** No resource was found for this request. */ + const val M_NOT_FOUND = "M_NOT_FOUND" + /** Too many requests have been sent in a short period of time. Wait a while then try again. */ + const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" + + /* ========================================================================================== + * Other error codes the client might encounter are + * ========================================================================================== */ + + /** Encountered when trying to register a user ID which has been taken. */ + const val M_USER_IN_USE = "M_USER_IN_USE" + /** Sent when the room alias given to the createRoom API is already in use. */ + const val M_ROOM_IN_USE = "M_ROOM_IN_USE" + /** (Not documented yet) */ + const val M_BAD_PAGINATION = "M_BAD_PAGINATION" + /** The request was not correctly authorized. Usually due to login failures. */ + const val M_UNAUTHORIZED = "M_UNAUTHORIZED" + /** (Not documented yet) */ + const val M_OLD_VERSION = "M_OLD_VERSION" + /** The server did not understand the request. */ + const val M_UNRECOGNIZED = "M_UNRECOGNIZED" + /** (Not documented yet) */ + const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET" + /** Authentication could not be performed on the third party identifier. */ + const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" + /** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */ + const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND" + /** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */ + const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE" + /** The client's request used a third party server, eg. identity server, that this server does not trust. */ + const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" + /** The request or entity was too large. */ + const val M_TOO_LARGE = "M_TOO_LARGE" + /** (Not documented yet) */ + const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" + /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example, + * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory + * or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach + * out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account + * data, etc) and not routes which only read state (eg: /sync, get account data, etc). */ + const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" + /** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */ + const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED" + /** Encountered when trying to register a user ID which is not valid. */ + const val M_INVALID_USERNAME = "M_INVALID_USERNAME" + /** Sent when the initial state given to the createRoom API is invalid. */ + const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE" + /** The server does not permit this third party identifier. This may happen if the server only permits, + * for example, email addresses from a particular domain. */ + const val M_THREEPID_DENIED = "M_THREEPID_DENIED" + /** The client's request to create a room used a room version that the server does not support. */ + const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION" + /** The client attempted to join a room that has a version the server does not support. + * Inspect the room_version property of the error response for the room's version. */ + const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION" + /** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */ + const val M_BAD_STATE = "M_BAD_STATE" + /** The room or resource does not permit guests to access it. */ + const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN" + /** A Captcha is required to complete the request. */ + const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" + /** The Captcha provided did not match what was expected. */ + const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID" + /** A required parameter was missing from the request. */ + const val M_MISSING_PARAM = "M_MISSING_PARAM" + /** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */ + const val M_INVALID_PARAM = "M_INVALID_PARAM" + /** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */ + const val M_EXCLUSIVE = "M_EXCLUSIVE" + /** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */ + const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" + /** (Not documented yet) */ + const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + + const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" + + // For identity service + const val M_INVALID_PEPPER = "M_INVALID_PEPPER" + + // Possible value for "limit_type" + const val LIMIT_TYPE_MAU = "monthly_active_user" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1296c8aa3dd3c99570efc71f38e2587ba1d9ee6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.interfaces + +/** + * Can be implemented by any object containing a timestamp. + * This interface can be use to sort such object + */ +interface DatedObject { + val date: Long +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..05128005ccb79e124df3e1c170e7324252cbd429 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.legacy + +interface LegacySessionImporter { + + /** + * Will eventually import a session created by the legacy app. + * @return true if a session has been imported + */ + fun process(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d6c9387f19ef08ab3a725fbd2e90b964401f32f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.listeners + +/** + * Interface to send a progress info + */ +interface ProgressListener { + /** + * Will be invoked on the background thread, not in UI thread. + * @param progress from 0 to total by contract + * @param total + */ + fun onProgress(progress: Int, total: Int) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..afe6ac51bda9b9f9035d98e5a820d863e5b2f44f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.listeners + +/** + * Interface to send a progress info + */ +interface StepProgressListener { + + sealed class Step { + data class ComputingKey(val progress: Int, val total: Int) : Step() + object DownloadingKey : Step() + data class ImportingKey(val progress: Int, val total: Int) : Step() + } + + /** + * @param step The current step, containing progress data if available. Else you should consider progress as indeterminate + */ + fun onStepProgress(step: Step) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7e719d6db6dfc5ef2bb1e2bf5962f3fdd8e26ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.text.Spannable + +/** + * MatrixLinkify take a piece of text and turns all of the + * matrix patterns matches in the text into clickable links. + */ +object MatrixLinkify { + + /** + * Find the matrix spans i.e matrix id , user id ... to display them as URL. + * + * @param spannable the text in which the matrix items has to be clickable. + */ + @Suppress("UNUSED_PARAMETER") + fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { + /** + * I disable it because it mess up with pills, and even with pills, it does not work correctly: + * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to + */ + /* + // sanity checks + if (spannable.isEmpty()) { + return false + } + val text = spannable.toString() + var hasMatch = false + for (pattern in MatrixPatterns.MATRIX_PATTERNS) { + for (match in pattern.findAll(spannable)) { + hasMatch = true + val startPos = match.range.first + if (startPos == 0 || text[startPos - 1] != '/') { + val endPos = match.range.last + 1 + val url = text.substring(match.range) + val span = MatrixPermalinkSpan(url, callback) + spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + return hasMatch + */ + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt new file mode 100644 index 0000000000000000000000000000000000000000..15957d359a0da5da05c1516f81dc8353b3d286da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.text.style.ClickableSpan +import android.view.View +import org.matrix.android.sdk.api.permalinks.MatrixPermalinkSpan.Callback + +/** + * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. + * @param url the permalink url tied to the span + * @param callback the callback to use. + */ +class MatrixPermalinkSpan(private val url: String, + private val callback: Callback? = null) : ClickableSpan() { + + interface Callback { + fun onUrlClicked(url: String) + } + + override fun onClick(widget: View) { + callback?.onUrlClicked(url) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt new file mode 100644 index 0000000000000000000000000000000000000000..3955c850c5937deb6d27da9131a804fcbae91e58 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.net.Uri + +/** + * This sealed class represents all the permalink cases. + * You don't have to instantiate yourself but should use [PermalinkParser] instead. + */ +sealed class PermalinkData { + + data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData() + + data class UserLink(val userId: String) : PermalinkData() + + data class GroupLink(val groupId: String) : PermalinkData() + + data class FallbackLink(val uri: Uri) : PermalinkData() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..87b42f5ae8bd815aa97c755bf3c007ceac083566 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.permalinks + +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Useful methods to create Matrix permalink (matrix.to links). + */ +object PermalinkFactory { + + const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" + + /** + * Creates a permalink for an event. + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param event the event + * @return the permalink, or null in case of error + */ + fun createPermalink(event: Event): String? { + if (event.roomId.isNullOrEmpty() || event.eventId.isNullOrEmpty()) { + return null + } + return createPermalink(event.roomId, event.eventId) + } + + /** + * Creates a permalink for an id (can be a user Id, Room Id, etc.). + * Ex: "https://matrix.to/#/@benoit:matrix.org" + * + * @param id the id + * @return the permalink, or null in case of error + */ + fun createPermalink(id: String): String? { + return if (id.isEmpty()) { + null + } else MATRIX_TO_URL_BASE + escape(id) + } + + /** + * Creates a permalink for an event. If you have an event you can use [.createPermalink] + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param roomId the id of the room + * @param eventId the id of the event + * @return the permalink + */ + fun createPermalink(roomId: String, eventId: String): String { + return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + } + + /** + * Extract the linked id from the universal link + * + * @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org" + * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink + */ + fun getLinkedId(url: String): String? { + val isSupported = url.startsWith(MATRIX_TO_URL_BASE) + + return if (isSupported) { + url.substring(MATRIX_TO_URL_BASE.length) + } else null + } + + /** + * Escape '/' in id, because it is used as a separator + * + * @param id the id to escape + * @return the escaped id + */ + internal fun escape(id: String): String { + return id.replace("/", "%2F") + } + + /** + * Unescape '/' in id + * + * @param id the id to escape + * @return the escaped id + */ + internal fun unescape(id: String): String { + return id.replace("%2F", "/") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..4cf9331f0e56c64f61960a48f6f1c361a2427564 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.net.Uri +import org.matrix.android.sdk.api.MatrixPatterns + +/** + * This class turns an uri to a [PermalinkData] + */ +object PermalinkParser { + + /** + * Turns an uri string to a [PermalinkData] + */ + fun parse(uriString: String): PermalinkData { + val uri = Uri.parse(uriString) + return parse(uri) + } + + /** + * Turns an uri to a [PermalinkData] + */ + fun parse(uri: Uri): PermalinkData { + if (!uri.toString().startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) { + return PermalinkData.FallbackLink(uri) + } + + val fragment = uri.fragment + if (fragment.isNullOrEmpty()) { + return PermalinkData.FallbackLink(uri) + } + + val indexOfQuery = fragment.indexOf("?") + val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment + + // we are limiting to 2 params + val params = safeFragment + .split(MatrixPatterns.SEP_REGEX.toRegex()) + .filter { it.isNotEmpty() } + .take(2) + + val identifier = params.getOrNull(0) + val extraParameter = params.getOrNull(1) + return when { + identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) + MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) + MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) + MatrixPatterns.isRoomId(identifier) -> { + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) + } + MatrixPatterns.isRoomAlias(identifier) -> { + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) + } + else -> PermalinkData.FallbackLink(uri) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a21920e586bb7873ac1836173fc16d0dfda7f1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import timber.log.Timber + +sealed class Action { + object Notify : Action() + object DoNotNotify : Action() + data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action() + data class Highlight(val highlight: Boolean) : Action() +} + +private const val ACTION_NOTIFY = "notify" +private const val ACTION_DONT_NOTIFY = "dont_notify" +private const val ACTION_COALESCE = "coalesce" + +// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks +private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak" + +private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound" +private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight" + +private const val ACTION_OBJECT_VALUE_KEY = "value" +private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#actions + * + * Convert + * <pre> + * "actions": [ + * "notify", + * { + * "set_tweak": "sound", + * "value": "default" + * }, + * { + * "set_tweak": "highlight" + * } + * ] + * + * To + * [ + * Action.Notify, + * Action.Sound("default"), + * Action.Highlight(true) + * ] + * + * </pre> + */ + +@Suppress("IMPLICIT_CAST_TO_ANY") +fun List<Action>.toJson(): List<Any> { + return map { action -> + when (action) { + is Action.Notify -> ACTION_NOTIFY + is Action.DoNotNotify -> ACTION_DONT_NOTIFY + is Action.Sound -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, + ACTION_OBJECT_VALUE_KEY to action.sound + ) + } + is Action.Highlight -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT, + ACTION_OBJECT_VALUE_KEY to action.highlight + ) + } + } + } +} + +fun PushRule.getActions(): List<Action> { + val result = ArrayList<Action>() + + actions.forEach { actionStrOrObj -> + when (actionStrOrObj) { + ACTION_NOTIFY -> Action.Notify + ACTION_DONT_NOTIFY -> Action.DoNotNotify + is Map<*, *> -> { + when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) { + ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue -> + Action.Sound(stringValue) + } + // When the value is not there, default sound (not specified by the spec) + ?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT) + } + ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue -> + Action.Highlight(boolValue) + } + // When the value is not there, default is true, says the spec + ?: Action.Highlight(true) + } + else -> { + Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}") + null + } + } + } + else -> { + Timber.w("Unsupported action type $actionStrOrObj") + null + } + }?.let { + result.add(it) + } + } + + return result +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt new file mode 100644 index 0000000000000000000000000000000000000000..50c2f8505bd3446262fe3e7cd96108397e1356b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event + +abstract class Condition(val kind: Kind) { + + enum class Kind(val value: String) { + EventMatch("event_match"), + ContainsDisplayName("contains_display_name"), + RoomMemberCount("room_member_count"), + SenderNotificationPermission("sender_notification_permission"), + Unrecognised(""); + + companion object { + + fun fromString(value: String): Kind { + return when (value) { + "event_match" -> EventMatch + "contains_display_name" -> ContainsDisplayName + "room_member_count" -> RoomMemberCount + "sender_notification_permission" -> SenderNotificationPermission + else -> Unrecognised + } + } + } + } + + abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean + + open fun technicalDescription(): String { + return "Kind: $kind" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc92ce8d29fc24ff99e87cf75ed4091cfb7a09f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Acts like a visitor on Conditions. + * This class as all required context needed to evaluate rules + */ +interface ConditionResolver { + fun resolveEventMatchCondition(event: Event, + condition: EventMatchCondition): Boolean + + fun resolveRoomMemberCountCondition(event: Event, + condition: RoomMemberCountCondition): Boolean + + fun resolveSenderNotificationPermissionCondition(event: Event, + condition: SenderNotificationPermissionCondition): Boolean + + fun resolveContainsDisplayNameCondition(event: Event, + condition: ContainsDisplayNameCondition): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt new file mode 100644 index 0000000000000000000000000000000000000000..a836c24c4eb761efac94e0664a25ae737fc476cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import timber.log.Timber + +class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveContainsDisplayNameCondition(event, this) + } + + override fun technicalDescription(): String { + return "User is mentioned" + } + + fun isSatisfied(event: Event, displayName: String): Boolean { + val message = when (event.type) { + EventType.MESSAGE -> { + event.content.toModel<MessageContent>() + } + // TODO the spec says: + // Matches any message whose content is unencrypted and contains the user's current display name + // EventType.ENCRYPTED -> { + // event.root.getClearContent()?.toModel<MessageContent>() + // } + else -> null + } ?: return false + + return caseInsensitiveFind(displayName, message.body) + } + + companion object { + /** + * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. + * + * @param subString the string to search for + * @param longString the string to search in + * @return whether a match was found + */ + fun caseInsensitiveFind(subString: String, longString: String): Boolean { + // add sanity checks + if (subString.isEmpty() || longString.isEmpty()) { + return false + } + + try { + val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE) + return regex.containsMatchIn(longString) + } catch (e: Exception) { + Timber.e(e, "## caseInsensitiveFind() : failed") + } + + return false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt new file mode 100644 index 0000000000000000000000000000000000000000..5eed785899143b9a173a777a0e4d1a9a5d62224c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber + +class EventMatchCondition( + /** + * The dot-separated field of the event to match, e.g. content.body + */ + val key: String, + /** + * The glob-style pattern to match against. Patterns with no special glob characters should + * be treated as having asterisks prepended and appended when testing the condition. + */ + val pattern: String +) : Condition(Kind.EventMatch) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveEventMatchCondition(event, this) + } + + override fun technicalDescription(): String { + return "'$key' Matches '$pattern'" + } + + fun isSatisfied(event: Event): Boolean { + // TODO encrypted events? + val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *> + ?: return false + val value = extractField(rawJson, key) ?: return false + + // Patterns with no special glob characters should be treated as having asterisks prepended + // and appended when testing the condition. + try { + val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*") + val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) + return regex.containsMatchIn(value) + } catch (e: Throwable) { + // e.g PatternSyntaxException + Timber.e(e, "Failed to evaluate push condition") + return false + } + } + + private fun extractField(jsonObject: Map<*, *>, fieldPath: String): String? { + val fieldParts = fieldPath.split(".") + if (fieldParts.isEmpty()) return null + + var jsonElement: Map<*, *> = jsonObject + fieldParts.forEachIndexed { index, pathSegment -> + if (index == fieldParts.lastIndex) { + return jsonElement[pathSegment]?.toString() + } else { + val sub = jsonElement[pathSegment] ?: return null + if (sub is Map<*, *>) { + jsonElement = sub + } else { + return null + } + } + } + return null + } + + companion object { + + private fun hasSpecialGlobChar(glob: String): Boolean { + return glob.contains("*") || glob.contains("?") + } + + // Very simple glob to regexp converter + private fun simpleGlobToRegExp(glob: String): String { + var out = "" // "^" + for (element in glob) { + when (element) { + '*' -> out += ".*" + '?' -> out += '.'.toString() + '.' -> out += "\\." + '\\' -> out += "\\\\" + else -> out += element + } + } + out += "" // '$'.toString() + return out + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt new file mode 100644 index 0000000000000000000000000000000000000000..64ccdcdeced92122813b3580692d28c6cc398738 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.rest.RuleSet +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable + +interface PushRuleService { + /** + * Fetch the push rules from the server + */ + fun fetchPushRules(scope: String = RuleScope.GLOBAL) + + fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet + + fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable + + fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable + + fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable + + fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable + + fun addPushRuleListener(listener: PushRuleListener) + + fun removePushRuleListener(listener: PushRuleListener) + +// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? + + interface PushRuleListener { + fun onMatchRule(event: Event, actions: List<Action>) + fun onRoomJoined(roomId: String) + fun onRoomLeft(roomId: String) + fun onEventRedacted(redactedEventId: String) + fun batchFinish() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt new file mode 100644 index 0000000000000000000000000000000000000000..f97636a7bdc6a0b1eb8278ede13ee2ec3b8d5bbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.RoomGetter +import timber.log.Timber + +private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") + +class RoomMemberCountCondition( + /** + * A decimal integer optionally prefixed by one of ==, <, >, >= or <=. + * A prefix of < matches rooms where the member count is strictly less than the given number and so forth. + * If no prefix is present, this parameter defaults to ==. + */ + val iz: String +) : Condition(Kind.RoomMemberCount) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveRoomMemberCountCondition(event, this) + } + + override fun technicalDescription(): String { + return "Room member count is $iz" + } + + internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean { + // sanity checks + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + + // Parse the is field into prefix and number the first time + val (prefix, count) = parseIsField() ?: return false + + val numMembers = room.getNumberOfJoinedMembers() + + return when (prefix) { + "<" -> numMembers < count + ">" -> numMembers > count + "<=" -> numMembers <= count + ">=" -> numMembers >= count + else -> numMembers == count + } + } + + /** + * Parse the is field to extract meaningful information. + */ + private fun parseIsField(): Pair<String?, Int>? { + try { + val match = regex.find(iz) ?: return null + val (prefix, count) = match.destructured + return prefix to count.toInt() + } catch (t: Throwable) { + Timber.e(t, "Unable to parse 'is' field") + } + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt new file mode 100644 index 0000000000000000000000000000000000000000..eeb2577d4c3395286455069eb0787c1477105381 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules + +/** + * Known rule ids + * + * Ref: https://matrix.org/docs/spec/client_server/latest#predefined-rules + */ +object RuleIds { + // Default Override Rules + const val RULE_ID_DISABLE_ALL = ".m.rule.master" + const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices" + const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me" + const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event" + const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name" + + const val RULE_ID_TOMBSTONE = ".m.rule.tombstone" + const val RULE_ID_ROOM_NOTIF = ".m.rule.roomnotif" + + // Default Content Rules + const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name" + + // Default Underride Rules + const val RULE_ID_CALL = ".m.rule.call" + const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one" + const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" + const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" + const val RULE_ID_ENCRYPTED = ".m.rule.encrypted" + + // Not documented + const val RULE_ID_FALLBACK = ".m.rule.fallback" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt new file mode 100644 index 0000000000000000000000000000000000000000..d94026f438553d1fe1f8a9060d9423163632debb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +object RuleScope { + const val GLOBAL = "global" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..f716b33f2390bd74107d11cde7caf00c8047ae4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +enum class RuleSetKey(val value: String) { + CONTENT("content"), + OVERRIDE("override"), + ROOM("room"), + SENDER("sender"), + UNDERRIDE("underride") +} + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid + */ +typealias RuleKind = RuleSetKey diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8d08e54586bfdf6c93f654427c1c35bdf8b24c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper + +class SenderNotificationPermissionCondition( + /** + * A string that determines the power level the sender must have to trigger notifications of a given type, + * such as room. Refer to the m.room.power_levels event schema for information about what the defaults are + * and how to interpret the event. The key is used to look up the power level required to send a notification + * type from the notifications object in the power level event content. + */ + val key: String +) : Condition(Kind.SenderNotificationPermission) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveSenderNotificationPermissionCondition(event, this) + } + + override fun technicalDescription(): String { + return "User power level <$key>" + } + + fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { + val powerLevelsHelper = PowerLevelsHelper(powerLevels) + return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..f83d893c0a3df7998115d0298dcd092c78740f3e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * All push rulesets for a user. + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +internal data class GetPushRulesResponse( + /** + * Global rules, account level applying to all devices + */ + @Json(name = "global") + val global: RuleSet, + + /** + * Device specific rules, apply only to current device + */ + @Json(name = "device") + val device: RuleSet? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt new file mode 100644 index 0000000000000000000000000000000000000000..9469da3ea5e99e4b681d461b904b0a80544c3002 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition +import org.matrix.android.sdk.api.pushrules.EventMatchCondition +import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition +import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition +import timber.log.Timber + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class PushCondition( + /** + * Required. The kind of condition to apply. + */ + @Json(name = "kind") + val kind: String, + + /** + * Required for event_match conditions. The dot- separated field of the event to match. + */ + @Json(name = "key") + val key: String? = null, + + /** + * Required for event_match conditions. + */ + @Json(name = "pattern") + val pattern: String? = null, + + /** + * Required for room_member_count conditions. + * A decimal integer optionally prefixed by one of, ==, <, >, >= or <=. + * A prefix of < matches rooms where the member count is strictly less than the given number and so forth. + * If no prefix is present, this parameter defaults to ==. + */ + @Json(name = "is") + val iz: String? = null +) { + + fun asExecutableCondition(): Condition? { + return when (Condition.Kind.fromString(kind)) { + Condition.Kind.EventMatch -> { + if (key != null && pattern != null) { + EventMatchCondition(key, pattern) + } else { + Timber.e("Malformed Event match condition") + null + } + } + Condition.Kind.ContainsDisplayName -> { + ContainsDisplayNameCondition() + } + Condition.Kind.RoomMemberCount -> { + if (iz.isNullOrEmpty()) { + Timber.e("Malformed ROOM_MEMBER_COUNT condition") + null + } else { + RoomMemberCountCondition(iz) + } + } + Condition.Kind.SenderNotificationPermission -> { + if (key == null) { + Timber.e("Malformed Sender Notification Permission condition") + null + } else { + SenderNotificationPermissionCondition(key) + } + } + Condition.Kind.Unrecognised -> { + Timber.e("Unknown kind $kind") + null + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt new file mode 100644 index 0000000000000000000000000000000000000000..46d73a8aa28fa8208ebb6505daad9eb1566a014e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.toJson + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class PushRule( + /** + * Required. The actions to perform when this rule is matched. + */ + @Json(name = "actions") + val actions: List<Any>, + /** + * Required. Whether this is a default rule, or has been set explicitly. + */ + @Json(name = "default") + val default: Boolean? = false, + /** + * Required. Whether the push rule is enabled or not. + */ + @Json(name = "enabled") + val enabled: Boolean, + /** + * Required. The ID of this rule. + */ + @Json(name = "rule_id") + val ruleId: String, + /** + * The conditions that must hold true for an event in order for a rule to be applied to an event + */ + @Json(name = "conditions") + val conditions: List<PushCondition>? = null, + /** + * The glob-style pattern to match against. Only applicable to content rules. + */ + @Json(name = "pattern") + val pattern: String? = null +) { + /** + * Add the default notification sound. + */ + fun setNotificationSound(): PushRule { + return setNotificationSound(ACTION_VALUE_DEFAULT) + } + + fun getNotificationSound(): String? { + return (getActions().firstOrNull { it is Action.Sound } as? Action.Sound)?.sound + } + + /** + * Set the notification sound + * + * @param sound notification sound + */ + fun setNotificationSound(sound: String): PushRule { + return copy( + actions = (getActions().filter { it !is Action.Sound } + Action.Sound(sound)).toJson() + ) + } + + /** + * Remove the notification sound + */ + fun removeNotificationSound(): PushRule { + return copy( + actions = getActions().filter { it !is Action.Sound }.toJson() + ) + } + + /** + * Set the highlight status. + * + * @param highlight the highlight status + */ + fun setHighlight(highlight: Boolean): PushRule { + return copy( + actions = (getActions().filter { it !is Action.Highlight } + Action.Highlight(highlight)).toJson() + ) + } + + /** + * Set the notification status. + * + * @param notify true to notify + */ + fun setNotify(notify: Boolean): PushRule { + val mutableActions = actions.toMutableList() + + mutableActions.remove(ACTION_DONT_NOTIFY) + mutableActions.remove(ACTION_NOTIFY) + + if (notify) { + mutableActions.add(ACTION_NOTIFY) + } else { + mutableActions.add(ACTION_DONT_NOTIFY) + } + + return copy(actions = mutableActions) + } + + /** + * Return true if the rule should highlight the event. + * + * @return true if the rule should play sound + */ + fun shouldNotify() = actions.contains(ACTION_NOTIFY) + + /** + * Return true if the rule should not highlight the event. + * + * @return true if the rule should not play sound + */ + fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY) + + companion object { + /* ========================================================================================== + * Rule id + * ========================================================================================== */ + + const val RULE_ID_DISABLE_ALL = ".m.rule.master" + const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name" + const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name" + const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" + const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me" + const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event" + const val RULE_ID_CALL = ".m.rule.call" + const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices" + const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" + const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif" + const val RULE_ID_TOMBSTONE = ".m.rule.tombstone" + const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one" + const val RULE_ID_E2E_GROUP = ".m.rule.encrypted" + const val RULE_ID_REACTION = ".m.rule.reaction" + const val RULE_ID_FALLBACK = ".m.rule.fallback" + + /* ========================================================================================== + * Actions + * ========================================================================================== */ + + const val ACTION_NOTIFY = "notify" + const val ACTION_DONT_NOTIFY = "dont_notify" + const val ACTION_COALESCE = "coalesce" + + const val ACTION_SET_TWEAK_SOUND_VALUE = "sound" + const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight" + + const val ACTION_PARAMETER_SET_TWEAK = "set_tweak" + const val ACTION_PARAMETER_VALUE = "value" + + const val ACTION_VALUE_DEFAULT = "default" + const val ACTION_VALUE_RING = "ring" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb813dba45f59c6c8447b12228111ed822b18c07 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.RuleSetKey + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class RuleSet( + @Json(name = "content") + val content: List<PushRule>? = null, + @Json(name = "override") + val override: List<PushRule>? = null, + @Json(name = "room") + val room: List<PushRule>? = null, + @Json(name = "sender") + val sender: List<PushRule>? = null, + @Json(name = "underride") + val underride: List<PushRule>? = null +) { + fun getAllRules(): List<PushRule> { + // Ref. for the order: https://matrix.org/docs/spec/client_server/latest#push-rules + return override.orEmpty() + content.orEmpty() + room.orEmpty() + sender.orEmpty() + underride.orEmpty() + } + + /** + * Find a rule from its ruleID. + * + * @param ruleId a RULE_ID_XX value + * @return the matched bing rule or null it doesn't exist. + */ + fun findDefaultRule(ruleId: String?): PushRuleAndKind? { + var result: PushRuleAndKind? = null + // sanity check + if (null != ruleId) { + if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) { + result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) } + } else { + // assume that the ruleId is unique. + result = findRule(override, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.OVERRIDE) } + if (null == result) { + result = findRule(underride, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.UNDERRIDE) } + } + } + } + return result + } + + /** + * Find a rule from its rule Id. + * + * @param rules the rules list. + * @param ruleId the rule Id. + * @return the bing rule if it exists, else null. + */ + private fun findRule(rules: List<PushRule>?, ruleId: String): PushRule? { + return rules?.firstOrNull { it.ruleId == ruleId } + } +} + +data class PushRuleAndKind( + val pushRule: PushRule, + val kind: RuleSetKey +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt new file mode 100644 index 0000000000000000000000000000000000000000..21ff3aebcaf559bf69db0965a29963050b9aa09d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.query + +/** + * Basic query language. All these cases are mutually exclusive. + */ +sealed class QueryStringValue { + object NoCondition : QueryStringValue() + object IsNull : QueryStringValue() + object IsNotNull : QueryStringValue() + object IsEmpty : QueryStringValue() + object IsNotEmpty : QueryStringValue() + data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + + enum class Case { + SENSITIVE, + INSENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt new file mode 100644 index 0000000000000000000000000000000000000000..42bb29efcac8b43f11ceed8a116a623735f99593 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData + +interface InitialSyncProgressService { + + fun getInitialSyncProgressStatus(): LiveData<Status> + + sealed class Status { + object Idle : Status() + data class Progressing( + @StringRes val statusText: Int, + val percentProgress: Int = 0 + ) : Status() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt new file mode 100644 index 0000000000000000000000000000000000000000..95370c01880228d1a37dfcc7d7ae2dfa2529d649 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session + +import androidx.annotation.MainThread +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.identity.IdentityService +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.widgets.WidgetService +import okhttp3.OkHttpClient + +/** + * This interface defines interactions with a session. + * An instance of a session will be provided by the SDK. + */ +interface Session : + RoomService, + RoomDirectoryService, + GroupService, + UserService, + CacheService, + SignOutService, + FilterService, + TermsService, + ProfileService, + PushRuleService, + PushersService, + InitialSyncProgressService, + HomeServerCapabilitiesService, + SecureStorageService, + AccountDataService, + AccountService { + + /** + * The params associated to the session + */ + val sessionParams: SessionParams + + /** + * The session is valid, i.e. it has a valid token so far + */ + val isOpenable: Boolean + + /** + * Useful shortcut to get access to the userId + */ + val myUserId: String + get() = sessionParams.userId + + /** + * The sessionId + */ + val sessionId: String + + /** + * This method allow to open a session. It does start some service on the background. + */ + @MainThread + fun open() + + /** + * Requires a one time background sync + */ + fun requireBackgroundSync() + + /** + * Launches infinite periodic background syncs + * This does not work in doze mode :/ + * If battery optimization is on it can work in app standby but that's all :/ + */ + fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) + + fun stopAnyBackgroundSync() + + /** + * This method start the sync thread. + */ + fun startSync(fromForeground: Boolean) + + /** + * This method stop the sync thread. + */ + fun stopSync() + + /** + * This method allows to listen the sync state. + * @return a [LiveData] of [SyncState]. + */ + fun getSyncStateLive(): LiveData<SyncState> + + /** + * This method returns the current sync state. + * @return the current [SyncState]. + */ + fun getSyncState(): SyncState + + /** + * This methods return true if an initial sync has been processed + */ + fun hasAlreadySynced(): Boolean + + /** + * This method allow to close a session. It does stop some services. + */ + fun close() + + /** + * Returns the ContentUrlResolver associated to the session. + */ + fun contentUrlResolver(): ContentUrlResolver + + /** + * Returns the ContentUploadProgressTracker associated with the session + */ + fun contentUploadProgressTracker(): ContentUploadStateTracker + + /** + * Returns the TypingUsersTracker associated with the session + */ + fun typingUsersTracker(): TypingUsersTracker + + /** + * Returns the ContentDownloadStateTracker associated with the session + */ + fun contentDownloadProgressTracker(): ContentDownloadStateTracker + + /** + * Returns the cryptoService associated with the session + */ + fun cryptoService(): CryptoService + + /** + * Returns the identity service associated with the session + */ + fun identityService(): IdentityService + + /** + * Returns the widget service associated with the session + */ + fun widgetService(): WidgetService + + /** + * Returns the integration manager service associated with the session + */ + fun integrationManagerService(): IntegrationManagerService + + /** + * Returns the call signaling service associated with the session + */ + fun callSignalingService(): CallSignalingService + + /** + * Returns the file download service associated with the session + */ + fun fileService(): FileService + + /** + * Add a listener to the session. + * @param listener the listener to add. + */ + fun addListener(listener: Listener) + + /** + * Remove a listener from the session. + * @param listener the listener to remove. + */ + fun removeListener(listener: Listener) + + /** + * Will return a OkHttpClient which will manage pinned certificates and Proxy if configured. + * It will not add any access-token to the request. + * So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client. + */ + fun getOkHttpClient(): OkHttpClient + + /** + * A global session listener to get notified for some events. + */ + interface Listener { + /** + * Possible cases: + * - The access token is not valid anymore, + * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver + */ + fun onGlobalError(globalError: GlobalError) + } + + val sharedSecretStorageService: SharedSecretStorageService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt new file mode 100644 index 0000000000000000000000000000000000000000..40c373820cf791d095c43ea9876ccbdde2343856 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.account + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to manage the account. It's implemented at the session level. + */ +interface AccountService { + /** + * Ask the homeserver to change the password. + * @param password Current password. + * @param newPassword New password + */ + fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Deactivate the account. + * + * This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register + * the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account + * details from your identity server. <b>This action is irreversible</b>.\n\nDeactivating your account <b>does not by default + * cause us to forget messages you have sent</b>. If you would like us to forget your messages, please tick the box below. + * + * Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not + * be shared with any new or unregistered users, but registered users who already have access to these messages will still + * have access to their copy. + * + * @param password the account password + * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see + * an incomplete view of conversations + */ + fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt new file mode 100644 index 0000000000000000000000000000000000000000..a90a34de4b6f7fd37e32f5636b465af82c8fca27 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.accountdata + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +interface AccountDataService { + /** + * Retrieve the account data with the provided type or null if not found + */ + fun getAccountDataEvent(type: String): UserAccountDataEvent? + + /** + * Observe the account data with the provided type + */ + fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> + + /** + * Retrieve the account data with the provided types. The return list can have a different size that + * the size of the types set, because some AccountData may not exist. + * If an empty set is provided, all the AccountData are retrieved + */ + fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> + + /** + * Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed + */ + fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> + + /** + * Update the account data with the provided type and the provided account data content + */ + fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>? = null): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..57eda657ac9b2b371c1d28485f8c5eae17349b45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * This is a simplified Event with just a type and a content. + * Currently used types are defined in [UserAccountDataTypes]. + */ +@JsonClass(generateAdapter = true) +data class UserAccountDataEvent( + @Json(name = "type") val type: String, + @Json(name = "content") val content: Content +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt new file mode 100644 index 0000000000000000000000000000000000000000..2414e4a1fb5bbef0cbb8e95e32b8f6c720ecc2b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.accountdata + +object UserAccountDataTypes { + const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" + const val TYPE_DIRECT_MESSAGES = "m.direct" + const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" + const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" + const val TYPE_WIDGETS = "m.widgets" + const val TYPE_PUSH_RULES = "m.push_rules" + const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" + const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" + const val TYPE_IDENTITY_SERVER = "m.identity_server" + const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt new file mode 100644 index 0000000000000000000000000000000000000000..a36856a7e6c1c88e7d1686ddbd3d9cd6e605c475 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.cache + +import org.matrix.android.sdk.api.MatrixCallback + +/** + * This interface defines a method to clear the cache. It's implemented at the session level. + */ +interface CacheService { + + /** + * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. + */ + fun clearCache(callback: MatrixCallback<Unit>) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..2962f9fac346a762342913fb125e3500f1cb8968 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface CallSignalingService { + + fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable + + /** + * Create an outgoing call + */ + fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall + + fun addCallListener(listener: CallsListener) + + fun removeCallListener(listener: CallsListener) + + fun getCallWithId(callId: String) : MxCall? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt new file mode 100644 index 0000000000000000000000000000000000000000..60268abf70065635849891d3157eb80111c2e0ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import org.webrtc.PeerConnection + +sealed class CallState { + + /** Idle, setting up objects */ + object Idle : CallState() + + /** Dialing. Outgoing call is signaling the remote peer */ + object Dialing : CallState() + + /** Local ringing. Incoming call offer received */ + object LocalRinging : CallState() + + /** Answering. Incoming call is responding to remote peer */ + object Answering : CallState() + + /** + * Connected. Incoming/Outgoing call, ice layer connecting or connected + * Notice that the PeerState failed is not always final, if you switch network, new ice candidtates + * could be exchanged, and the connection could go back to connected + * */ + data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState() + + /** Terminated. Incoming/Outgoing call, the call is terminated */ + object Terminated : CallState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..81430c71ea9f1e27607a21dd8a92be4a94002a78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent + +interface CallsListener { + /** + * Called when there is an incoming call within the room. + */ + fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) + + fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) + + /** + * An outgoing call is started. + */ + fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) + + /** + * Called when a called has been hung up + */ + fun onCallHangupReceived(callHangupContent: CallHangupContent) + + fun onCallManagedByOtherSession(callId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b70d8500fef599b289d54c511209ba4681553e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.call + +import org.webrtc.EglBase +import timber.log.Timber + +/** + * The root [EglBase] instance shared by the entire application for + * the sake of reducing the utilization of system resources (such as EGL + * contexts) + * by performing a runtime check. + */ +object EglUtils { + + // TODO how do we release that? + + /** + * Lazily creates and returns the one and only [EglBase] which will + * serve as the root for all contexts that are needed. + */ + @get:Synchronized var rootEglBase: EglBase? = null + get() { + if (field == null) { + val configAttributes = EglBase.CONFIG_PLAIN + try { + field = EglBase.createEgl14(configAttributes) + ?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10. + } catch (ex: Throwable) { + Timber.e(ex, "Failed to create EglBase") + } + } + return field + } + private set + + val rootEglBaseContext: EglBase.Context? + get() { + val eglBase = rootEglBase + return eglBase?.eglBaseContext + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt new file mode 100644 index 0000000000000000000000000000000000000000..04af588b9330af1c8685bcd2597a28a4eabbee87 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import org.webrtc.IceCandidate +import org.webrtc.SessionDescription + +interface MxCallDetail { + val callId: String + val isOutgoing: Boolean + val roomId: String + val otherUserId: String + val isVideoCall: Boolean +} + +/** + * Define both an incoming call and on outgoing call + */ +interface MxCall : MxCallDetail { + + var state: CallState + /** + * Pick Up the incoming call + * It has no effect on outgoing call + */ + fun accept(sdp: SessionDescription) + + /** + * Reject an incoming call + * It's an alias to hangUp + */ + fun reject() = hangUp() + + /** + * End the call + */ + fun hangUp() + + /** + * Start a call + * Send offer SDP to the other participant. + */ + fun offerSdp(sdp: SessionDescription) + + /** + * Send Ice candidate to the other participant. + */ + fun sendLocalIceCandidates(candidates: List<IceCandidate>) + + /** + * Send removed ICE candidates to the other participant. + */ + fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) + + fun addListener(listener: StateListener) + fun removeListener(listener: StateListener) + + interface StateListener { + fun onStateUpdate(call: MxCall) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..f63a1a0d2831c50304d50100ed688ce1d72d1631 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// TODO Should not be exposed +/** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver + */ +@JsonClass(generateAdapter = true) +data class TurnServerResponse( + /** + * Required. The username to use. + */ + @Json(name = "username") val username: String?, + + /** + * Required. The password to use. + */ + @Json(name = "password") val password: String?, + + /** + * Required. A list of TURN URIs + */ + @Json(name = "uris") val uris: List<String>?, + + /** + * Required. The time-to-live in seconds + */ + @Json(name = "ttl") val ttl: Int? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt new file mode 100644 index 0000000000000000000000000000000000000000..045a9bc1a0ca9b95062a09eaa07704b6deac21ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.content + +import android.net.Uri +import android.os.Parcelable +import androidx.exifinterface.media.ExifInterface +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class ContentAttachmentData( + val size: Long = 0, + val duration: Long? = 0, + val date: Long = 0, + val height: Long? = 0, + val width: Long? = 0, + val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, + val name: String? = null, + val queryUri: Uri, + private val mimeType: String?, + val type: Type +) : Parcelable { + + enum class Type { + FILE, + IMAGE, + AUDIO, + VIDEO + } + + fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..a29e7110e2dae8413e7d43266aebdd93554d901f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.content + +interface ContentUploadStateTracker { + + fun track(key: String, updateListener: UpdateListener) + + fun untrack(key: String, updateListener: UpdateListener) + + fun clear() + + interface UpdateListener { + fun onUpdate(state: State) + } + + sealed class State { + object Idle : State() + object EncryptingThumbnail : State() + data class UploadingThumbnail(val current: Long, val total: Long) : State() + object Encrypting : State() + data class Uploading(val current: Long, val total: Long) : State() + object Success : State() + data class Failure(val throwable: Throwable) : State() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..890e72edd9f090f2adc71e24ad23f0f2caf25a70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.content + +/** + * This interface defines methods for accessing content from the current session. + */ +interface ContentUrlResolver { + + enum class ThumbnailMethod(val value: String) { + CROP("crop"), + SCALE("scale") + } + + /** + * URL to use to upload content + */ + val uploadUrl: String + + /** + * Get the actual URL for accessing the full-size image of a Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @return the URL to access the described resource, or null if the url is invalid. + */ + fun resolveFullSize(contentUrl: String?): String? + + /** + * Get the actual URL for accessing the thumbnail image of a given Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param width the desired width + * @param height the desired height + * @param method the desired method (METHOD_CROP or METHOD_SCALE) + * @return the URL to access the described resource, or null if the url is invalid. + */ + fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt new file mode 100644 index 0000000000000000000000000000000000000000..726f9b624a3f204fb20c5aa951e219bee08f1257 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto + +import android.content.Context +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody + +interface CryptoService { + + fun verificationService(): VerificationService + + fun crossSigningService(): CrossSigningService + + fun keysBackupService(): KeysBackupService + + fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) + + fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) + + fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) + + fun getCryptoVersion(context: Context, longFormat: Boolean): String + + fun isCryptoEnabled(): Boolean + + fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean + + fun setWarnOnUnknownDevices(warn: Boolean) + + fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) + + fun getUserDevices(userId: String): MutableList<CryptoDeviceInfo> + + fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) + + fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? + + fun getMyDevice(): CryptoDeviceInfo + + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) + + fun getDeviceTrackingStatus(userId: String): Int + + fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>) + + fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) + + fun setRoomBlacklistUnverifiedDevices(roomId: String) + + fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + + fun requestRoomKeyForEvent(event: Event) + + fun reRequestRoomKeyForEvent(event: Event) + + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) + + fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) + + fun getMyDevicesInfo() : List<DeviceInfo> + + fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>> + + fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) + + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + fun isRoomEncrypted(roomId: String): Boolean + + fun encryptEventContent(eventContent: Content, + eventType: String, + roomId: String, + callback: MatrixCallback<MXEncryptEventContentResult>) + + fun discardOutboundSession(roomId: String) + + @Throws(MXCryptoError::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + + fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) + + fun getEncryptionAlgorithm(roomId: String): String? + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>) + + fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> + + fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> + + fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> + + fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> + + fun addNewSessionListener(newSessionListener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) + + fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> + + fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> + + fun getGossipingEventsTrail(): List<Event> + + // For testing shared session + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int> + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt new file mode 100644 index 0000000000000000000000000000000000000000..53bee09f118a2acec1cf41e7f8c27f2c4b9af85b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.olm.OlmException + +/** + * Represents a crypto error response. + */ +sealed class MXCryptoError : Throwable() { + + data class Base(val errorType: ErrorType, + val technicalMessage: String, + /** + * Describe the error with more details + */ + val detailedErrorDescription: String? = null) : MXCryptoError() + + data class OlmError(val olmException: OlmException) : MXCryptoError() + + data class UnknownDevice(val deviceList: MXUsersDevicesMap<CryptoDeviceInfo>) : MXCryptoError() + + enum class ErrorType { + ENCRYPTING_NOT_ENABLED, + UNABLE_TO_ENCRYPT, + UNABLE_TO_DECRYPT, + UNKNOWN_INBOUND_SESSION_ID, + INBOUND_SESSION_MISMATCH_ROOM_ID, + MISSING_FIELDS, + BAD_EVENT_FORMAT, + MISSING_SENDER_KEY, + MISSING_CIPHER_TEXT, + BAD_DECRYPTED_FORMAT, + NOT_INCLUDE_IN_RECIPIENTS, + BAD_RECIPIENT, + BAD_RECIPIENT_KEY, + FORWARDED_MESSAGE, + BAD_ROOM, + BAD_ENCRYPTED_MESSAGE, + DUPLICATED_MESSAGE_INDEX, + MISSING_PROPERTY, + OLM, + UNKNOWN_DEVICES, + UNKNOWN_MESSAGE_INDEX, + KEYS_WITHHELD + } + + companion object { + /** + * Resource for technicalMessage + */ + const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s" + const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s" + const val OLM_REASON = "OLM error: %1\$s" + const val DETAILED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s" + const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id" + const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)" + const val MISSING_FIELDS_REASON = "Missing fields in input" + const val BAD_EVENT_FORMAT_TEXT_REASON = "Bad event format" + const val MISSING_SENDER_KEY_TEXT_REASON = "Missing senderKey" + const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext" + const val BAD_DECRYPTED_FORMAT_TEXT_REASON = "Bad decrypted event format" + const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients" + const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s" + const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device" + const val FORWARDED_MESSAGE_REASON = "Message forwarded from %1\$s" + const val BAD_ROOM_REASON = "Message intended for room %1\$s" + const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message" + const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s" + const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack" + const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + + "We strongly recommend you verify them before continuing." + const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + + " Perhaps the homeserver is hiding the configuration event." + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt new file mode 100644 index 0000000000000000000000000000000000000000..490b1d19c144e60472d599a70c3ad0c98826c4c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult +import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo + +interface CrossSigningService { + + fun isCrossSigningVerified(): Boolean + + fun isUserTrusted(otherUserId: String): Boolean + + /** + * Will not force a download of the key, but will verify signatures trust chain. + * Checks that my trusted user key has signed the other user UserKey + */ + fun checkUserTrust(otherUserId: String): UserTrustResult + + /** + * Initialize cross signing for this user. + * Users needs to enter credentials + */ + fun initializeCrossSigning(authParams: UserPasswordAuth?, + callback: MatrixCallback<Unit>) + + fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null + + fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String?): UserTrustResult + + fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + + fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> + + fun getMyCrossSigningKeys(): MXCrossSigningInfo? + + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + + fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> + + fun canCrossSign(): Boolean + + fun allPrivateKeysKnown(): Boolean + + fun trustUser(otherUserId: String, + callback: MatrixCallback<Unit>) + + fun markMyMasterKeyAsTrusted() + + /** + * Sign one of your devices and upload the signature + */ + fun trustDevice(deviceId: String, + callback: MatrixCallback<Unit>) + + fun checkDeviceTrust(otherUserId: String, + otherDeviceId: String, + locallyTrusted: Boolean?): DeviceTrustResult + + // FIXME Those method do not have to be in the service + fun onSecretMSKGossip(mskPrivateKey: String) + fun onSecretSSKGossip(sskPrivateKey: String) + fun onSecretUSKGossip(uskPrivateKey: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc85254f69f20aef4c5753bfdefb64d10bfbf5db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master" + +const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing" + +const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing" + +const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..0212dee36c5317043d289a14d25c001a57369413 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.KeyUsage + +data class MXCrossSigningInfo( + val userId: String, + val crossSigningKeys: List<CryptoCrossSigningKey> +) { + + fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true + && selfSigningKey()?.trustLevel?.isVerified() == true + + fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true } + + fun userKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.USER_SIGNING.value) == true } + + fun selfSigningKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.SELF_SIGNING.value) == true } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ceeb87c128b6970e1bfa240dd80c4b2744307a0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo + +interface KeysBackupService { + /** + * Retrieve the current version of the backup from the home server + * + * It can be different than keysBackupVersion. + * @param callback onSuccess(null) will be called if there is no backup on the server + */ + fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) + + /** + * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion]. + * + * @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion]. + * @param callback Asynchronous callback + */ + fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback<KeysVersion>) + + /** + * Facility method to get the total number of locally stored keys + */ + fun getTotalNumbersOfKeys(): Int + + /** + * Facility method to get the number of backed up keys + */ + fun getTotalNumbersOfBackedUpKeys(): Int + + /** + * Start to back up keys immediately. + * + * @param progressListener the callback to follow the progress + * @param callback the main callback + */ + fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback<Unit>?) + + /** + * Check trust on a key backup version. + * + * @param keysBackupVersion the backup version to check. + * @param callback block called when the operations completes. + */ + fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback<KeysBackupVersionTrust>) + + /** + * Return the current progress of the backup + */ + fun getBackupProgress(progressListener: ProgressListener) + + /** + * Get information about a backup version defined on the homeserver. + * + * It can be different than keysBackupVersion. + * @param version the backup version + * @param callback + */ + fun getVersion(version: String, + callback: MatrixCallback<KeysVersionResult?>) + + /** + * This method fetches the last backup version on the server, then compare to the currently backup version use. + * If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version. + * + * @param callback true if backup is already using the last version, and false if it is not the case + */ + fun forceUsingLastVersion(callback: MatrixCallback<Boolean>) + + /** + * Check the server for an active key backup. + * + * If one is present and has a valid signature from one of the user's verified + * devices, start backing up to it. + */ + fun checkAndStartKeysBackup() + + fun addListener(listener: KeysBackupStateListener) + + fun removeListener(listener: KeysBackupStateListener) + + /** + * Set up the data required to create a new backup version. + * The backup version will not be created and enabled until [createKeysBackupVersion] + * is called. + * The returned [MegolmBackupCreationInfo] object has a `recoveryKey` member with + * the user-facing recovery key string. + * + * @param password an optional passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * @param progressListener a progress listener, as generating private key from password may take a while + * @param callback Asynchronous callback + */ + fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback<MegolmBackupCreationInfo>) + + /** + * Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself. + * If we are backing up to this version. Backup will be stopped. + * + * @param version the backup version to delete. + * @param callback Asynchronous callback + */ + fun deleteBackup(version: String, + callback: MatrixCallback<Unit>?) + + /** + * Ask if the backup on the server contains keys that we may do not have locally. + * This should be called when entering in the state READY_TO_BACKUP + */ + fun canRestoreKeys(): Boolean + + /** + * Set trust on a keys backup version. + * It adds (or removes) the signature of the current device to the authentication part of the keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param trust the trust to set to the keys backup. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback<Unit>) + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param recoveryKey the recovery key to challenge with the key backup public key. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback<Unit>) + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param password the pass phrase to challenge with the keyBackupVersion public key. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback<Unit>) + + fun onSecretKeyGossip(secret: String) + + /** + * Restore a backup with a recovery key from a given backup version stored on the homeserver. + * + * @param keysVersionResult the backup version to restore from. + * @param recoveryKey the recovery key to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. + * @param stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback<ImportRoomKeysResult>) + + /** + * Restore a backup with a password from a given backup version stored on the homeserver. + * + * @param keysBackupVersion the backup version to restore from. + * @param password the password to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. + * @param stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback<ImportRoomKeysResult>) + + val keysBackupVersion: KeysVersionResult? + val currentBackupVersion: String? + val isEnabled: Boolean + val isStucked: Boolean + val state: KeysBackupState + + // For gossiping + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ab190ac98b37e9e4ff0b902f775d1e866da197c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +/** + * E2e keys backup states. + * + * <pre> + * | + * V deleteKeyBackupVersion (on current backup) + * +----------------------> UNKNOWN <------------- + * | | + * | | checkAndStartKeysBackup (at startup or on new verified device or a new detected backup) + * | V + * | CHECKING BACKUP + * | | + * | Network error | + * +<----------+----------------+-------> DISABLED <----------------------+ + * | | | | | + * | | | | createKeysBackupVersion | + * | V | V | + * +<--- WRONG VERSION | ENABLING | + * | ^ | | | + * | | V ok | error | + * | | +------> READY <--------+----------------------------+ + * V | | | + * NOT TRUSTED | | | on new key + * | | V + * | | WILL BACK UP (waiting a random duration) + * | | | + * | | | + * | | ok V + * | +----- BACKING UP + * | | + * | Error | + * +<---------------+ + * </pre> + */ +enum class KeysBackupState { + // Need to check the current backup version on the homeserver + Unknown, + // Checking if backup is enabled on home server + CheckingBackUpOnHomeserver, + // Backup has been stopped because a new backup version has been detected on the homeserver + WrongBackUpVersion, + // Backup from this device is not enabled + Disabled, + // There is a backup available on the homeserver but it is not trusted. + // It is not trusted because the signature is invalid or the device that created it is not verified + // Use [KeysBackup.getKeysBackupTrust()] to get trust details. + // Consequently, the backup from this device is not enabled. + NotTrusted, + // Backup is being enabled: the backup version is being created on the homeserver + Enabling, + // Backup is enabled and ready to send backup to the homeserver + ReadyToBackUp, + // e2e keys are going to be sent to the homeserver + WillBackUp, + // e2e keys are being sent to the homeserver + BackingUp +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..10cfe6ce85d5ed50dafc4ffeb929eebb077ace05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +interface KeysBackupStateListener { + + /** + * The keys backup state has changed + * @param newState the new state + */ + fun onStateChange(newState: KeysBackupState) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..3daee31bcf3099be7e7267fa7fd1e1a40ba06366 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.keyshare + +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingRequestCancellation +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest + +/** + * Room keys events listener + */ +interface GossipingRequestListener { + /** + * An room key request has been received. + * + * @param request the request + */ + fun onRoomKeyRequest(request: IncomingRoomKeyRequest) + + /** + * Returns the secret value to be shared + * @return true if is handled + */ + fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt new file mode 100644 index 0000000000000000000000000000000000000000..acd0866401f0ee1b1f00e2c5d4e0b767c8fdbf7e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +enum class CancelCode(val value: String, val humanReadable: String) { + User("m.user", "the user cancelled the verification"), + Timeout("m.timeout", "the verification process timed out"), + UnknownTransaction("m.unknown_transaction", "the device does not know about that transaction"), + UnknownMethod("m.unknown_method", "the device can’t agree on a key agreement, hash, MAC, or SAS method"), + MismatchedCommitment("m.mismatched_commitment", "the hash commitment did not match"), + MismatchedSas("m.mismatched_sas", "the SAS did not match"), + UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"), + InvalidMessage("m.invalid_message", "an invalid message was received"), + MismatchedKeys("m.key_mismatch", "Key mismatch"), + UserError("m.user_error", "User error"), + MismatchedUser("m.user_mismatch", "User mismatch"), + QrCodeInvalid("m.qr_code.invalid", "Invalid QR code") +} + +fun safeValueOf(code: String?): CancelCode { + return CancelCode.values().firstOrNull { code == it.value } ?: CancelCode.User +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b568ee1431fddd74efd9a665388bdf2e3dcbeaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +data class EmojiRepresentation(val emoji: String, + @StringRes val nameResId: Int, + @DrawableRes val drawableRes: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..45d04e66f0282877303c428780e19be983d0c040 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface IncomingSasVerificationTransaction : SasVerificationTransaction { + val uxState: UxState + + fun performAccept() + + enum class UxState { + UNKNOWN, + SHOW_ACCEPT, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..c6be940cde8145507698acead6d97b57ba1ecd64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface OutgoingSasVerificationTransaction : SasVerificationTransaction { + val uxState: UxState + + enum class UxState { + UNKNOWN, + WAIT_FOR_START, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8da60976ac8b4b77340f4566ecab0cfd0cd0ed8f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.crypto.verification + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import java.util.UUID + +/** + * Stores current pending verification requests + */ +data class PendingVerificationRequest( + val ageLocalTs: Long, + val isIncoming: Boolean = false, + val localId: String = UUID.randomUUID().toString(), + val otherUserId: String, + val roomId: String?, + val transactionId: String? = null, + val requestInfo: ValidVerificationInfoRequest? = null, + val readyInfo: ValidVerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful: Boolean = false, + val handledByOtherSession: Boolean = false, + // In case of to device it is sent to a list of devices + val targetDevices: List<String>? = null +) { + val isReady: Boolean = readyInfo != null + val isSent: Boolean = transactionId != null + + val isFinished: Boolean = isSuccessful || cancelConclusion != null + + /** + * SAS is supported if I support it and the other party support it + */ + fun isSasSupported(): Boolean { + return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + } + + /** + * Other can show QR code if I can scan QR code and other can show QR code + */ + fun otherCanShowQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } + } + + /** + * Other can scan QR code if I can show QR code and other can scan QR code + */ + fun otherCanScanQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4956aaabbed20cb77dbaf5a748cfa4c04a10b3d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface QrCodeVerificationTransaction : VerificationTransaction { + + /** + * To use to display a qr code, for the other user to scan it + */ + val qrCodeText: String? + + /** + * Call when you have scan the other user QR code + */ + fun userHasScannedOtherQrCode(otherQrCodeText: String) + + /** + * Call when you confirm that other user has scanned your QR code + */ + fun otherUserScannedMyQrCode() + + /** + * Call when you do not confirm that other user has scanned your QR code + */ + fun otherUserDidNotScannedMyQrCode() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt new file mode 100644 index 0000000000000000000000000000000000000000..2dc5c308ee2b318ce6e6bdb16408c10898766a81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +object SasMode { + const val DECIMAL = "decimal" + const val EMOJI = "emoji" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..00da238bd251d86e287078d7e54408aaea19888d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface SasVerificationTransaction : VerificationTransaction { + + fun supportsEmoji(): Boolean + + fun supportsDecimal(): Boolean + + fun getEmojiCodeRepresentation(): List<EmojiRepresentation> + + fun getDecimalCodeRepresentation(): String + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + fun userHasVerifiedShortCode() + + fun shortCodeDoesNotMatch() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt new file mode 100644 index 0000000000000000000000000000000000000000..68c1bb7bb09e86b7012dcb769c1be13bad333e3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +data class ValidVerificationInfoReady( + val transactionId: String, + val fromDevice: String, + val methods: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..431c9728ee05b7b1d24716d9a9d4c4eaca4ddc08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +data class ValidVerificationInfoRequest( + val transactionId: String, + val fromDevice: String, + val methods: List<String>, + val timestamp: Long? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt new file mode 100644 index 0000000000000000000000000000000000000000..15a728ccf2c3b283029a9c8d7842a5785fa93127 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +/** + * Verification methods + */ +enum class VerificationMethod { + // Use it when your application supports the SAS verification method + SAS, + // Use it if your application is able to display QR codes + QR_CODE_SHOW, + // Use it if your application is able to scan QR codes + QR_CODE_SCAN +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..623f9e5c0ef8942bc52582b9feaa5b6bd72e0795 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho + +/** + * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework + * + * Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors. + * Verification is a user-friendly key verification process. + * Verification is intended to be a highly interactive process for users, + * and as such exposes verification methods which are easier for users to use. + */ +interface VerificationService { + + fun addListener(listener: Listener) + + fun removeListener(listener: Listener) + + /** + * Mark this device as verified manually + */ + fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + + fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? + + fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>? + + fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? + + fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? + + fun beginKeyVerification(method: VerificationMethod, + otherUserId: String, + otherDeviceId: String, + transactionId: String?): String? + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, + otherUserId: String, + roomId: String, + localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest + + fun cancelVerificationRequest(request: PendingVerificationRequest) + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerification(methods: List<VerificationMethod>, + otherUserId: String, + otherDevices: List<String>?): PendingVerificationRequest + + fun declineVerificationRequestInDMs(otherUserId: String, + transactionId: String, + roomId: String) + + // Only SAS method is supported for the moment + // TODO Parameter otherDeviceId should be removed in this case + fun beginKeyVerificationInDMs(method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback<String>?): String? + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerificationInDMs(methods: List<VerificationMethod>, + otherUserId: String, + roomId: String, + transactionId: String): Boolean + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerification(methods: List<VerificationMethod>, + otherUserId: String, + transactionId: String): Boolean + + interface Listener { + /** + * Called when a verification request is created either by the user, or by the other user. + */ + fun verificationRequestCreated(pr: PendingVerificationRequest) {} + + /** + * Called when a verification request is updated. + */ + fun verificationRequestUpdated(pr: PendingVerificationRequest) {} + + /** + * Called when a transaction is created, either by the user or initiated by the other user. + */ + fun transactionCreated(tx: VerificationTransaction) {} + + /** + * Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction. + */ + fun transactionUpdated(tx: VerificationTransaction) {} + + /** + * Inform the the deviceId of the userId has been marked as manually verified by the SDK. + * It will be called after VerificationService.markedLocallyAsManuallyVerified() is called. + * + */ + fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + + companion object { + + private const val TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000 + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + + fun isValidRequest(age: Long?): Boolean { + if (age == null) return false + val now = System.currentTimeMillis() + val tooInThePast = now - TEN_MINUTES_IN_MILLIS + val tooInTheFuture = now + FIVE_MINUTES_IN_MILLIS + return age in tooInThePast..tooInTheFuture + } + } + + fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..7e7dcb6d9024425bb16a2234ad67a798337fd30d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface VerificationTransaction { + + var state: VerificationTxState + + val transactionId: String + val otherUserId: String + var otherDeviceId: String? + + // TODO Not used. Remove? + val isIncoming: Boolean + + /** + * User wants to cancel the transaction + */ + fun cancel() + + fun cancel(code: CancelCode) + + fun isToDeviceTransport(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8ae81bc304364b3fa4cf55edc28f7e73ce3d045 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +sealed class VerificationTxState { + // Uninitialized state + object None : VerificationTxState() + + // Specific for SAS + abstract class VerificationSasTxState : VerificationTxState() + + object SendingStart : VerificationSasTxState() + object Started : VerificationSasTxState() + object OnStarted : VerificationSasTxState() + object SendingAccept : VerificationSasTxState() + object Accepted : VerificationSasTxState() + object OnAccepted : VerificationSasTxState() + object SendingKey : VerificationSasTxState() + object KeySent : VerificationSasTxState() + object OnKeyReceived : VerificationSasTxState() + object ShortCodeReady : VerificationSasTxState() + object ShortCodeAccepted : VerificationSasTxState() + object SendingMac : VerificationSasTxState() + object MacSent : VerificationSasTxState() + object Verifying : VerificationSasTxState() + + // Specific for QR code + abstract class VerificationQrTxState : VerificationTxState() + + // Will be used to ask the user if the other user has correctly scanned + object QrScannedByOther : VerificationQrTxState() + object WaitingOtherReciprocateConfirm : VerificationQrTxState() + + // Terminal states + abstract class TerminalTxState : VerificationTxState() + + object Verified : TerminalTxState() + + // Cancelled by me or by other + data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt new file mode 100644 index 0000000000000000000000000000000000000000..30a1e29d81f25467cc107dfba24c83288bedcbfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * <code> + * { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "ðŸ‘", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * </code> + */ + +@JsonClass(generateAdapter = true) +data class AggregatedAnnotation( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List<RelationChunkInfo>? = null + +) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bc1af25e013485a39295115f5ae3c08a47f03f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * <code> + * { + * "m.annotation": { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "ðŸ‘", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * "m.reference": { + * "chunk": [ + * { + * "type": "m.room.message", + * "event_id": "$some_event_id" + * } + * ], + * "limited": false, + * "count": 1 + * } + * } + * </code> + */ + +@JsonClass(generateAdapter = true) +data class AggregatedRelations( + @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, + @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8be9e26a03108c57df507180a25e806fe34c951 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DefaultUnsignedRelationInfo( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List<Map<String, Any>>? = null + +) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt new file mode 100644 index 0000000000000000000000000000000000000000..fdd3e667036959be57b5d6a7a59fa0bbb63b766d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.json.JSONObject +import timber.log.Timber + +typealias Content = JsonDict + +/** + * This methods is a facility method to map a json content to a model. + */ +inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return try { + moshiAdapter.fromJsonValue(this) + } catch (e: Exception) { + if (catchError) { + Timber.e(e, "To model failed : $e") + null + } else { + throw e + } + } +} + +/** + * This methods is a facility method to map a model to a json Content + */ +@Suppress("UNCHECKED_CAST") +inline fun <reified T> T.toContent(): Content { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.toJsonValue(this) as Content +} + +/** + * Generic event class with all possible fields for events. + * The content and prevContent json fields can easily be mapped to a model with [toModel] method. + */ +@JsonClass(generateAdapter = true) +data class Event( + @Json(name = "type") val type: String, + @Json(name = "event_id") val eventId: String? = null, + @Json(name = "content") val content: Content? = null, + @Json(name = "prev_content") val prevContent: Content? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, + @Json(name = "sender") val senderId: String? = null, + @Json(name = "state_key") val stateKey: String? = null, + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null, + @Json(name = "redacts") val redacts: String? = null +) { + + @Transient + var mxDecryptionResult: OlmDecryptionResult? = null + + @Transient + var mCryptoError: MXCryptoError.ErrorType? = null + + @Transient + var mCryptoErrorReason: String? = null + + @Transient + var sendState: SendState = SendState.UNKNOWN + + /** + * The `age` value transcoded in a timestamp based on the device clock when the SDK received + * the event from the home server. + * Unlike `age`, this value is static. + */ + @Transient + var ageLocalTs: Long? = null + + /** + * Check if event is a state event. + * @return true if event is state event. + */ + fun isStateEvent(): Boolean { + return stateKey != null + } + + // ============================================================================================================== + // Crypto + // ============================================================================================================== + + /** + * @return true if this event is encrypted. + */ + fun isEncrypted(): Boolean { + return type == EventType.ENCRYPTED + } + + /** + * @return The curve25519 key that sent this event. + */ + fun getSenderKey(): String? { + return mxDecryptionResult?.senderKey + } + + /** + * @return The additional keys the sender of this encrypted event claims to possess. + */ + fun getKeysClaimed(): Map<String, String> { + return mxDecryptionResult?.keysClaimed ?: HashMap() + } + + /** + * @return the event type + */ + fun getClearType(): String { + return mxDecryptionResult?.payload?.get("type")?.toString() ?: type + } + + /** + * @return the event content + */ + fun getClearContent(): Content? { + @Suppress("UNCHECKED_CAST") + return mxDecryptionResult?.payload?.get("content") as? Content ?: content + } + + fun toContentStringWithIndent(): String { + val contentMap = toContent() + return JSONObject(contentMap).toString(4) + } + + fun toClearContentStringWithIndent(): String? { + val contentMap = this.mxDecryptionResult?.payload + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } + } + + /** + * Tells if the event is redacted + */ + fun isRedacted() = unsignedData?.redactedEvent != null + + /** + * Tells if the event is redacted by the user himself. + */ + fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId + + fun resolvedPrevContent(): Content? = prevContent ?: unsignedData?.prevContent + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Event + + if (type != other.type) return false + if (eventId != other.eventId) return false + if (content != other.content) return false + if (prevContent != other.prevContent) return false + if (originServerTs != other.originServerTs) return false + if (senderId != other.senderId) return false + if (stateKey != other.stateKey) return false + if (roomId != other.roomId) return false + if (unsignedData != other.unsignedData) return false + if (redacts != other.redacts) return false + if (mxDecryptionResult != other.mxDecryptionResult) return false + if (mCryptoError != other.mCryptoError) return false + if (mCryptoErrorReason != other.mCryptoErrorReason) return false + if (sendState != other.sendState) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + (eventId?.hashCode() ?: 0) + result = 31 * result + (content?.hashCode() ?: 0) + result = 31 * result + (prevContent?.hashCode() ?: 0) + result = 31 * result + (originServerTs?.hashCode() ?: 0) + result = 31 * result + (senderId?.hashCode() ?: 0) + result = 31 * result + (stateKey?.hashCode() ?: 0) + result = 31 * result + (roomId?.hashCode() ?: 0) + result = 31 * result + (unsignedData?.hashCode() ?: 0) + result = 31 * result + (redacts?.hashCode() ?: 0) + result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) + result = 31 * result + (mCryptoError?.hashCode() ?: 0) + result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) + result = 31 * result + sendState.hashCode() + return result + } +} + +fun Event.isTextMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel<MessageContent>()?.msgType) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE -> true + else -> false + } +} + +fun Event.isImageMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel<MessageContent>()?.msgType) { + MessageType.MSGTYPE_IMAGE -> true + else -> false + } +} + +fun Event.isVideoMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel<MessageContent>()?.msgType) { + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} + +fun Event.isFileMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel<MessageContent>()?.msgType) { + MessageType.MSGTYPE_FILE -> true + else -> false + } +} + +fun Event.getRelationContent(): RelationDefaultContent? { + return if (isEncrypted()) { + content.toModel<EncryptedEventContent>()?.relatesTo + } else { + content.toModel<MessageContent>()?.relatesTo + } +} + +fun Event.isReply(): Boolean { + return getRelationContent()?.inReplyTo?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9f2e10af4db82097129725f0f3f96cdac4f42bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +/** + * Constants defining known event types from Matrix specifications. + */ +object EventType { + + const val PRESENCE = "m.presence" + const val MESSAGE = "m.room.message" + const val STICKER = "m.sticker" + const val ENCRYPTED = "m.room.encrypted" + const val FEEDBACK = "m.room.message.feedback" + const val TYPING = "m.typing" + const val REDACTION = "m.room.redaction" + const val RECEIPT = "m.receipt" + const val TAG = "m.tag" + const val ROOM_KEY = "m.room_key" + const val FULLY_READ = "m.fully_read" + const val PLUMBING = "m.room.plumbing" + const val BOT_OPTIONS = "m.room.bot.options" + const val PREVIEW_URLS = "org.matrix.room.preview_urls" + + // State Events + + const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets" + const val STATE_ROOM_WIDGET = "m.widget" + const val STATE_ROOM_NAME = "m.room.name" + const val STATE_ROOM_TOPIC = "m.room.topic" + const val STATE_ROOM_AVATAR = "m.room.avatar" + const val STATE_ROOM_MEMBER = "m.room.member" + const val STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite" + const val STATE_ROOM_CREATE = "m.room.create" + const val STATE_ROOM_JOIN_RULES = "m.room.join_rules" + const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" + const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" + const val STATE_ROOM_ALIASES = "m.room.aliases" + const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" + const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" + const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" + const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_ENCRYPTION = "m.room.encryption" + + // Call Events + const val CALL_INVITE = "m.call.invite" + const val CALL_CANDIDATES = "m.call.candidates" + const val CALL_ANSWER = "m.call.answer" + const val CALL_HANGUP = "m.call.hangup" + + // Key share events + const val ROOM_KEY_REQUEST = "m.room_key_request" + const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" + const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld" + + const val REQUEST_SECRET = "m.secret.request" + const val SEND_SECRET = "m.secret.send" + + // Interactive key verification + const val KEY_VERIFICATION_START = "m.key.verification.start" + const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" + const val KEY_VERIFICATION_KEY = "m.key.verification.key" + const val KEY_VERIFICATION_MAC = "m.key.verification.mac" + const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" + const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" + + // Relation Events + const val REACTION = "m.reaction" + + // Unwedging + internal const val DUMMY = "m.dummy" + + fun isCallEvent(type: String): Boolean { + return type == CALL_INVITE + || type == CALL_CANDIDATES + || type == CALL_ANSWER + || type == CALL_HANGUP + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa3726d49e142e7564b16b81b70c3c2e0e023dd1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +import java.util.UUID + +object LocalEcho { + + private const val PREFIX = "\$local." + + fun isLocalEchoId(eventId: String) = eventId.startsWith(PREFIX) + + fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..72ab3e5c0e16f4cd3c04d64d82292a966dca6cc5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * <code> + * { + * "type": "m.reaction", + * "key": "ðŸ‘", + * "count": 3 + * } + * </code> + */ + +@JsonClass(generateAdapter = true) +data class RelationChunkInfo( + val type: String, + val key: String, + val count: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c18bd6c8ae3fb604629193e967f31cf52a9eaf1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +/** + * Constants defining known event relation types from Matrix specifications + */ +object RelationType { + /** Lets you define an event which annotates an existing event.*/ + const val ANNOTATION = "m.annotation" + /** Lets you define an event which replaces an existing event.*/ + const val REPLACE = "m.replace" + /** Lets you define an event which references an existing event.*/ + const val REFERENCE = "m.reference" + /** Lets you define an event which adds a response to an existing event.*/ + const val RESPONSE = "org.matrix.response" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt new file mode 100644 index 0000000000000000000000000000000000000000..a16d9ec5bdd5b3e7bf96fd406d502d0cf4eafdcc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UnsignedData( + /** + * The time in milliseconds that has elapsed since the event was sent. + * This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers + * is out of sync, which can cause the age to either be negative or greater than it actually is. + */ + @Json(name = "age") val age: Long?, + /** + * Optional. The event that redacted this event, if any. + */ + @Json(name = "redacted_because") val redactedEvent: Event? = null, + /** + * The client-supplied transaction ID, if the client being given the event is the same one which sent it. + */ + @Json(name = "transaction_id") val transactionId: String? = null, + /** + * Optional. The previous content for this event. If there is no previous content, this key will be missing. + */ + @Json(name = "prev_content") val prevContent: Map<String, Any>? = null, + @Json(name = "m.relations") val relations: AggregatedRelations? = null, + /** + * Optional. The eventId of the previous state event being replaced. + */ + @Json(name = "replaces_state") val replacesState: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..04371ae54bb77312a29e205ce9187cf6df894d88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +interface UnsignedRelationInfo { + val limited : Boolean? + val count: Int? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..52bf0ed05c18f5438a72864c08c32b7cfb2423c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.file + +interface ContentDownloadStateTracker { + fun track(key: String, updateListener: UpdateListener) + fun unTrack(key: String, updateListener: UpdateListener) + fun clear() + + sealed class State { + object Idle : State() + data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State() + object Decrypting : State() + object Success : State() + data class Failure(val errorCode: Int) : State() + } + + interface UpdateListener { + fun onDownloadStateUpdate(state: State) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt new file mode 100644 index 0000000000000000000000000000000000000000..da42bfa485d7d9c62efc0db90c71c75e70c7fbc8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.file + +import android.net.Uri +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import java.io.File + +/** + * This interface defines methods to get files. + */ +interface FileService { + + enum class DownloadMode { + /** + * Download file in external storage + */ + TO_EXPORT, + + /** + * Download file in cache + */ + FOR_INTERNAL_USE, + + /** + * Download file in file provider path + */ + FOR_EXTERNAL_SHARE + } + + enum class FileState { + IN_CACHE, + DOWNLOADING, + UNKNOWN + } + + /** + * Download a file. + * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. + */ + fun downloadFile( + downloadMode: DownloadMode, + id: String, + fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback<File>): Cancelable + + fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + + /** + * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION + * (if not other app won't be able to access it) + */ + fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? + + /** + * Get information on the given file. + * Mimetype should be the same one as passed to downloadFile (limitation for now) + */ + fun fileState(mxcUrl: String, mimeType: String?): FileState + + /** + * Clears all the files downloaded by the service + */ + fun clearCache() + + /** + * Get size of cached files + */ + fun getCacheSize(): Int +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..b456626ef705c8baf03293f8ded493d2d2f6b99d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.file + +import android.net.Uri +import androidx.core.content.FileProvider + +/** + * We have to declare our own file provider to avoid collision with apps using the sdk + * and having their own + */ +class MatrixSDKFileProvider : FileProvider() { + override fun getType(uri: Uri): String? { + return super.getType(uri) ?: "plain/text" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt new file mode 100644 index 0000000000000000000000000000000000000000..10435ee054b9b140ebdb00c180c64c6f4bf17e1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.group + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to interact within a group. + */ +interface Group { + val groupId: String + + /** + * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary. + * The SDK also takes care of refreshing group data every hour. + * @param callback : the matrix callback to be notified of success or failure + * @return a Cancelable to be able to cancel requests. + */ + fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt new file mode 100644 index 0000000000000000000000000000000000000000..6858db4646c2697e785598a355e59c17785b4764 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.group + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.group.model.GroupSummary + +/** + * This interface defines methods to get groups. It's implemented at the session level. + */ +interface GroupService { + + /** + * Get a group from a groupId + * @param groupId the groupId to look for. + * @return the group with groupId or null + */ + fun getGroup(groupId: String): Group? + + /** + * Get a groupSummary from a groupId + * @param groupId the groupId to look for. + * @return the groupSummary with groupId or null + */ + fun getGroupSummary(groupId: String): GroupSummary? + + /** + * Get a list of group summaries. This list is a snapshot of the data. + * @return the list of [GroupSummary] + */ + fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> + + /** + * Get a live list of group summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of [GroupSummary] + */ + fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf9535f271084b5b7b0995184cfc34725e3713a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.group + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams { + return GroupSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter group summaries + */ +data class GroupSummaryQueryParams( + val displayName: QueryStringValue, + val memberships: List<Membership> +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List<Membership> = Membership.all() + + fun build() = GroupSummaryQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..2633bdcdebedc58f17da5d863271c3d83c5859b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.group.model + +import org.matrix.android.sdk.api.session.room.model.Membership + +/** + * This class holds some data of a group. + * It can be retrieved through [org.matrix.android.sdk.api.session.group.GroupService] + */ +data class GroupSummary( + val groupId: String, + val membership: Membership, + val displayName: String = "", + val shortDescription: String = "", + val avatarUrl: String = "", + val roomIds: List<String> = emptyList(), + val userIds: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt new file mode 100644 index 0000000000000000000000000000000000000000..c463fe9e72d77907a4ea317c6321989e2cc60c4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.homeserver + +data class HomeServerCapabilities( + /** + * True if it is possible to change the password of the account. + */ + val canChangePassword: Boolean = true, + /** + * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet + */ + val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN, + /** + * Last version identity server and binding supported + */ + val lastVersionIdentityServerSupported: Boolean = false, + /** + * Default identity server url, provided in Wellknown + */ + val defaultIdentityServerUrl: String? = null, + /** + * Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms + * (as it was before) for various environments where this is desired. + */ + val adminE2EByDefault: Boolean = true +) { + companion object { + const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt new file mode 100644 index 0000000000000000000000000000000000000000..bcf1052b98b745488f40956506e1cded95ad56fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.homeserver + +/** + * This interface defines a method to retrieve the homeserver capabilities. + */ +interface HomeServerCapabilitiesService { + + /** + * Get the HomeServer capabilities + */ + fun getHomeServerCapabilities(): HomeServerCapabilities +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ac1720400e9a06e5b22096449d420e59214b278 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +data class FoundThreePid( + val threePid: ThreePid, + val matrixId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a4054114e6300504fc4819ad6b852d4543f8e76 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * Provides access to the identity server configuration and services identity server can provide + */ +interface IdentityService { + /** + * Return the default identity server of the user, which may have been provided at login time by the homeserver, + * or by the Well-known setup of the homeserver + * It may be different from the current configured identity server + */ + fun getDefaultIdentityServer(): String? + + /** + * Return the current identity server URL used by this account. Returns null if no identity server is configured. + */ + fun getCurrentIdentityServerUrl(): String? + + /** + * Check if the identity server is valid + * See https://matrix.org/docs/spec/identity_service/latest#status-check + * RiotX SDK only supports identity server API v2 + */ + fun isValidIdentityServer(url: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the identity server url. + * If successful, any previous identity server will be disconnected. + * In case of error, any previous identity server will remain configured. + * @param url the new url. + * @param callback will notify the user if change is successful. The String will be the final url of the identity server. + * The SDK can prepend "https://" for instance. + */ + fun setNewIdentityServer(url: String, callback: MatrixCallback<String>): Cancelable + + /** + * Disconnect (logout) from the current identity server + */ + fun disconnect(callback: MatrixCallback<Unit>): Cancelable + + /** + * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid + */ + fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will cancel a pending binding of threePid. + */ + fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid + */ + fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Submit the code that the identity server has sent to the user (in email or SMS) + * Once successful, you will have to call [finalizeBindThreePid] + * @param code the code sent to the user + */ + fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will perform the actual association of ThreePid and Matrix account + */ + fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Unbind a threePid + * The request will actually be done on the homeserver + */ + fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Search MatrixId of users providing email and phone numbers + */ + fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable + + /** + * Get the status of the current user's threePid + * A lookup will be performed, but also pending binding state will be restored + * + * @param threePids the list of threePid the user owns (retrieved form the homeserver) + * @param callback onSuccess will be called with a map of ThreePid -> SharedState + */ + fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable + + fun addListener(listener: IdentityServiceListener) + fun removeListener(listener: IdentityServiceListener) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9f8ccb9d3655877d3f9be92835f9558217e10d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +import org.matrix.android.sdk.api.failure.Failure + +sealed class IdentityServiceError : Failure.FeatureFailure() { + object OutdatedIdentityServer : IdentityServiceError() + object OutdatedHomeServer : IdentityServiceError() + object NoIdentityServerConfigured : IdentityServiceError() + object TermsNotSignedException : IdentityServiceError() + object BulkLookupSha256NotSupported : IdentityServiceError() + object BindingError : IdentityServiceError() + object NoCurrentBindingError : IdentityServiceError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..f01d4e97c31d7921929ca4e8f262dcdd5a095315 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +interface IdentityServiceListener { + fun onIdentityServerChange() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt new file mode 100644 index 0000000000000000000000000000000000000000..3dae4b43eedd71387fcbf39749229c43d25bc21c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +enum class SharedState { + SHARED, + NOT_SHARED, + BINDING_IN_PROGRESS +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt new file mode 100644 index 0000000000000000000000000000000000000000..de4e0a9a5acc1b841815574856d9f442721178a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.identity + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier + +sealed class ThreePid(open val value: String) { + data class Email(val email: String) : ThreePid(email) + data class Msisdn(val msisdn: String) : ThreePid(msisdn) +} + +internal fun ThreePid.toMedium(): String { + return when (this) { + is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL + is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN + } +} + +@Throws(NumberParseException::class) +internal fun ThreePid.Msisdn.getCountryCode(): String { + return with(PhoneNumberUtil.getInstance()) { + getRegionCodeForCountryCode(parse("+$msisdn", null).countryCode) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..0bee245537e438682bd5849464fb5896069105de --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.integrationmanager + +/** + * This class holds configuration of integration manager. + */ +data class IntegrationManagerConfig( + val uiUrl: String, + val restUrl: String, + val kind: Kind +) { + + // Order matters, first is preferred + /** + * The kind of config, it will reflect where the data is coming from. + */ + enum class Kind { + /** + * Defined in UserAccountData + */ + ACCOUNT, + /** + * Defined in Wellknown + */ + HOMESERVER, + /** + * Fallback value, hardcoded by the SDK + */ + DEFAULT + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt new file mode 100644 index 0000000000000000000000000000000000000000..003e8bc9aa4a088c3a2d1df5750fe91a92133492 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.integrationmanager + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This is the entry point to manage integration. You can grab an instance of this service through an active session. + */ +interface IntegrationManagerService { + + /** + * This listener allow you to observe change related to integrations. + */ + interface Listener { + /** + * Is called whenever integration is enabled or disabled, comes from user account data. + */ + fun onIsEnabledChanged(enabled: Boolean) { + // No-op + } + + /** + * Is called whenever configs from user account data or wellknown are updated. + */ + fun onConfigurationChanged(configs: List<IntegrationManagerConfig>) { + // No-op + } + + /** + * Is called whenever widget permissions from user account data are updated. + */ + fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) { + // No-op + } + } + + /** + * Adds a listener to observe changes. + */ + fun addListener(listener: Listener) + + /** + * Removes a previously added listener. + */ + fun removeListener(listener: Listener) + + /** + * Return the list of current configurations, sorted by kind. First one is preferred. + * See [IntegrationManagerConfig.Kind] + */ + fun getOrderedConfigs(): List<IntegrationManagerConfig> + + /** + * Return the preferred current configuration. + * See [IntegrationManagerConfig.Kind] + */ + fun getPreferredConfig(): IntegrationManagerConfig + + /** + * Returns true if integration is enabled, false otherwise. + */ + fun isIntegrationEnabled(): Boolean + + /** + * Offers to enable or disable the integration. + * @param enable the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable + + /** + * Offers to allow or disallow a widget. + * @param stateEventId the eventId of the state event defining the widget. + * @param allowed the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable + + /** + * Returns true if the widget is allowed, false otherwise. + * @param stateEventId the eventId of the state event defining the widget. + */ + fun isWidgetAllowed(stateEventId: String): Boolean + + /** + * Offers to allow or disallow a native widget domain. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable + + /** + * Returns true if the widget domain is allowed, false otherwise. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt new file mode 100644 index 0000000000000000000000000000000000000000..449c670983f7206ceda91ccd7f8cf3964214bee8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.profile + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to handling profile information. It's implemented at the session level. + */ +interface ProfileService { + + companion object Constants { + const val DISPLAY_NAME_KEY = "displayname" + const val AVATAR_URL_KEY = "avatar_url" + } + + /** + * Return the current display name for this user + * @param userId the userId param to look for + * + */ + fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable + + /** + * Update the display name for this user + * @param userId the userId to update the display name of + * @param newDisplayName the new display name of the user + */ + fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable + + /** + * Update the avatar for this user + * @param userId the userId to update the avatar of + * @param newAvatarUri the new avatar uri of the user + * @param fileName the fileName of selected image + */ + fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable + + /** + * Return the current avatarUrl for this user. + * @param userId the userId param to look for + * + */ + fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable + + /** + * Get the combined profile information for this user. + * This may return keys which are not limited to displayname or avatar_url. + * @param userId the userId param to look for + * + */ + fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable + + /** + * Get the current user 3Pids + */ + fun getThreePids(): List<ThreePid> + + /** + * Get the current user 3Pids Live + * @param refreshData set to true to fetch data from the homeserver + */ + fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt new file mode 100644 index 0000000000000000000000000000000000000000..6cc089e1522e80eb98dde19a128394948bd2054e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.pushers + +data class Pusher( + val pushKey: String, + val kind: String, + val appId: String, + val appDisplayName: String?, + val deviceDisplayName: String?, + val profileTag: String? = null, + val lang: String?, + val data: PusherData, + + val state: PusherState +) + +enum class PusherState { + UNREGISTERED, + REGISTERING, + UNREGISTERING, + REGISTERED, + FAILED_TO_REGISTER +} + +data class PusherData( + val url: String? = null, + val format: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt new file mode 100644 index 0000000000000000000000000000000000000000..f42721f485ee6022337f284e611952dbe225b743 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.pushers + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +interface PushersService { + + /** + * Refresh pushers from server state + */ + fun refreshPushers() + + /** + * Add a new HTTP pusher. + * Note that only `http` kind is supported by the SDK for now. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + * + * @param pushkey This is a unique identifier for this pusher. The value you should use for + * this is the routing or destination address information for the notification, + * for example, the APNS token for APNS or the Registration ID for GCM. If your + * notification client has no such concept, use any unique identifier. Max length, 512 chars. + * If the kind is "email", this is the email address to send notifications to. + * @param appId the application id + * This is a reverse-DNS style identifier for the application. It is recommended + * that this end with the platform, such that different platform versions get + * different app identifiers. Max length, 64 chars. + * @param profileTag This string determines which set of device specific rules this pusher executes. + * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). + * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. + * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. + * @param url The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. + * @param withEventIdOnly true to limit the push content to only id and not message content + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour + * + * @return A work request uuid. Can be used to listen to the status + * (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>)) + * @throws [InvalidParameterException] if a parameter is not correct + */ + fun addHttpPusher(pushkey: String, + appId: String, + profileTag: String, + lang: String, + appDisplayName: String, + deviceDisplayName: String, + url: String, + append: Boolean, + withEventIdOnly: Boolean): UUID + + /** + * Remove the http pusher + */ + fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Get the current pushers, as a LiveData + */ + fun getPushersLive(): LiveData<List<Pusher>> + + /** + * Get the current pushers + */ + fun getPushers(): List<Pusher> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt new file mode 100644 index 0000000000000000000000000000000000000000..3d8550adf0fa00f06077f5ec131d842e4595909f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to interact within a room. + */ +interface Room : + TimelineService, + SendService, + DraftService, + ReadService, + TypingService, + TagsService, + MembershipService, + StateService, + UploadsService, + ReportingService, + RoomCallService, + RelationService, + RoomCryptoService, + RoomPushRuleService { + + /** + * The roomId of this room + */ + val roomId: String + + /** + * A live [RoomSummary] associated with the room + * You can observe this summary to get dynamic data from this room. + */ + fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> + + /** + * A current snapshot of [RoomSummary] associated with the room + */ + fun roomSummary(): RoomSummary? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt new file mode 100644 index 0000000000000000000000000000000000000000..17d3a2a95afb39e58ecbc778463e749f804b1898 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to get and join public rooms. It's implemented at the session level. + */ +interface RoomDirectoryService { + + /** + * Get rooms from directory + */ + fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback<PublicRoomsResponse>): Cancelable + + /** + * Fetches the overall metadata about protocols supported by the homeserver. + * Includes both the available protocols and all fields required for queries against each protocol. + */ + fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt new file mode 100644 index 0000000000000000000000000000000000000000..1161dce518802bd0d8e0886217fe7c4fe5da6953 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to get rooms. It's implemented at the session level. + */ +interface RoomService { + + /** + * Create a room asynchronously + */ + fun createRoom(createRoomParams: CreateRoomParams, + callback: MatrixCallback<String>): Cancelable + + /** + * Join a room by id + * @param roomIdOrAlias the roomId or the room alias of the room to join + * @param reason optional reason for joining the room + * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. + */ + fun joinRoom(roomIdOrAlias: String, + reason: String? = null, + viaServers: List<String> = emptyList(), + callback: MatrixCallback<Unit>): Cancelable + + /** + * Get a room from a roomId + * @param roomId the roomId to look for. + * @return a room with roomId or null + */ + fun getRoom(roomId: String): Room? + + /** + * Get a roomSummary from a roomId or a room alias + * @param roomIdOrAlias the roomId or the alias of a room to look for. + * @return a matching room summary or null + */ + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? + + /** + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] + */ + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary> + + /** + * Get a live list of room summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[RoomSummary] + */ + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> + + /** + * Get a snapshot list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. + * @return the immutable list of [RoomSummary] + */ + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> + + /** + * Get a live list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. + * @return the [LiveData] of [RoomSummary] + */ + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> + + /** + * Inform the Matrix SDK that a room is displayed. + * The SDK will update the breadcrumbs in the user account data + */ + fun onRoomDisplayed(roomId: String): Cancelable + + /** + * Mark all rooms as read + */ + fun markAllAsRead(roomIds: List<String>, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Resolve a room alias to a room ID. + */ + fun getRoomIdByAlias(roomAlias: String, + searchOnServer: Boolean, + callback: MatrixCallback<Optional<String>>): Cancelable + + /** + * Return a live data of all local changes membership that happened since the session has been opened. + * It allows you to track this in your client to known what is currently being processed by the SDK. + * It won't know anything about change being done in other client. + * Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action + */ + fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> + + fun getExistingDirectRoomWithUser(otherUserId: String): Room? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..5af23f8e242a82b02ec321a564e06c99bb33e240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { + return RoomSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room summaries to use with: + * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class RoomSummaryQueryParams( + val roomId: QueryStringValue, + val displayName: QueryStringValue, + val canonicalAlias: QueryStringValue, + val memberships: List<Membership> +) { + + class Builder { + + var roomId: QueryStringValue = QueryStringValue.IsNotEmpty + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition + var memberships: List<Membership> = Membership.all() + + fun build() = RoomSummaryQueryParams( + roomId = roomId, + displayName = displayName, + canonicalAlias = canonicalAlias, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ec27fdd5d41121f5d805c7dc8b17239b7e13950 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.call + +/** + * This interface defines methods to handle calls in a room. It's implemented at the room level. + */ +interface RoomCallService { + /** + * Return true if calls (audio or video) can be performed on this Room + */ + fun canStartCall(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7f018bda8d5d3e373b7597efab1985fb4de4009 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.crypto + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +interface RoomCryptoService { + + fun isEncrypted(): Boolean + + fun encryptionAlgorithm(): String? + + fun shouldEncryptForInvitedMembers(): Boolean + + /** + * Enable encryption of the room + */ + fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, + callback: MatrixCallback<Unit>) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt new file mode 100644 index 0000000000000000000000000000000000000000..d70dae3454936b6ce59b9bc76a46fef814336cb8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.failure + +import org.matrix.android.sdk.api.failure.Failure + +sealed class CreateRoomFailure : Failure.FeatureFailure() { + + object CreatedWithTimeout: CreateRoomFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef15fbc7c14737173f7e6c6e005fc6572218dfc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.failure + +import org.matrix.android.sdk.api.failure.Failure + +sealed class JoinRoomFailure : Failure.FeatureFailure() { + + object JoinedWithTimeout : JoinRoomFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d13b0bf947763374f93e24890246b99e04f0519 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.members + +sealed class ChangeMembershipState() { + object Unknown : ChangeMembershipState() + object Joining : ChangeMembershipState() + data class FailedJoining(val throwable: Throwable) : ChangeMembershipState() + object Joined : ChangeMembershipState() + object Leaving : ChangeMembershipState() + data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState() + object Left : ChangeMembershipState() + + fun isInProgress() = this is Joining || this is Leaving + + fun isSuccessful() = this is Joined || this is Left + + fun isFailed() = this is FailedJoining || this is FailedLeaving +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c9a50dc0c6649ce26258b6a947250003b8f8ade --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.members + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to handling membership. It's implemented at the room level. + */ +interface MembershipService { + + /** + * This methods load all room members if it was done yet. + * @return a [Cancelable] + */ + fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable + + /** + * Return the roomMember with userId or null. + * @param userId the userId param to look for + * + * @return the roomMember with userId or null + */ + fun getRoomMember(userId: String): RoomMemberSummary? + + /** + * Return all the roomMembers of the room with params + * @param queryParams the params to query for + * @return a roomMember list. + */ + fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMemberSummary> + + /** + * Return all the roomMembers of the room filtered by memberships + * @param queryParams the params to query for + * @return a [LiveData] of roomMember list. + */ + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMemberSummary>> + + fun getNumberOfJoinedMembers(): Int + + /** + * Invite a user in the room + */ + fun invite(userId: String, + reason: String? = null, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Invite a user with email or phone number in the room + */ + fun invite3pid(threePid: ThreePid, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Ban a user from the room + */ + fun ban(userId: String, + reason: String? = null, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Unban a user from the room + */ + fun unban(userId: String, + reason: String? = null, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Kick a user from the room + */ + fun kick(userId: String, + reason: String? = null, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Join the room, or accept an invitation. + */ + fun join(reason: String? = null, + viaServers: List<String> = emptyList(), + callback: MatrixCallback<Unit>): Cancelable + + /** + * Leave the room, or reject an invitation. + */ + fun leave(reason: String? = null, + callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..39fc7598f6987e8c68aceea10c6f694f2674e09c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.members + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams { + return RoomMemberQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room members + */ +data class RoomMemberQueryParams( + val displayName: QueryStringValue, + val memberships: List<Membership>, + val userId: QueryStringValue, + val excludeSelf: Boolean +) { + + class Builder { + + var userId: QueryStringValue = QueryStringValue.NoCondition + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List<Membership> = Membership.all() + var excludeSelf: Boolean = false + + fun build() = RoomMemberQueryParams( + displayName = displayName, + memberships = memberships, + userId = userId, + excludeSelf = excludeSelf + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..721dcf4f2e776b04f1b29812d024db64ba9a709b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.events.model.Content + +data class EditAggregatedSummary( + val aggregatedContent: Content? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List<String>, + val localEchos: List<String>, + val lastEditTs: Long = 0 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..d1b0c894108057d6c7e3c25717471628ca20046f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +data class EventAnnotationsSummary( + var eventId: String, + var reactionsSummary: List<ReactionAggregatedSummary> = emptyList(), + var editSummary: EditAggregatedSummary? = null, + var pollResponseSummary: PollResponseAggregatedSummary? = null, + var referencesAggregatedSummary: ReferencesAggregatedSummary? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b3a333672fe52e3f544751c23eb802d5f896e67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Subclass representing a search API response + */ +@JsonClass(generateAdapter = true) +data class Invite( + @Json(name = "display_name") val displayName: String, + @Json(name = "signed") val signed: Signed + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt new file mode 100644 index 0000000000000000000000000000000000000000..fc89ff06df6cff9fa7edc272202955c190f8ccbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents the membership of a user on a room + */ +@JsonClass(generateAdapter = false) +enum class Membership(val value: String) { + + NONE("none"), + + @Json(name = "invite") + INVITE("invite"), + + @Json(name = "join") + JOIN("join"), + + @Json(name = "knock") + KNOCK("knock"), + + @Json(name = "leave") + LEAVE("leave"), + + @Json(name = "ban") + BAN("ban"); + + fun isLeft(): Boolean { + return this == KNOCK || this == LEAVE || this == BAN + } + + fun isActive(): Boolean { + return activeMemberships().contains(this) + } + + companion object { + fun activeMemberships(): List<Membership> { + return listOf(INVITE, JOIN) + } + + fun all(): List<Membership> { + return values().asList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..695a3353d5946c44300f0330f793256d083f768d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +data class PollResponseAggregatedSummary( + + var aggregatedContent: PollSummaryContent? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List<String>, + val localEchos: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..07d62a173c11e813ef3d4fa43a29487afda6bc39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.JsonClass + +/** + * Contains an aggregated summary info of the poll response. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class PollSummaryContent( + // Index of my vote + var myVote: Int? = null, + // Array of VoteInfo, list is constructed so that there is only one vote by user + // And that optionIndex is valid + var votes: List<VoteInfo>? = null +) { + + fun voteCount(): Int { + return votes?.size ?: 0 + } + + fun voteCountForOption(optionIndex: Int) : Int { + return votes?.filter { it.optionIndex == optionIndex }?.count() ?: 0 + } +} + +@JsonClass(generateAdapter = true) +data class VoteInfo( + val userId: String, + val optionIndex: Int, + val voteTimestamp: Long +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..e55508c9dbf0a6934c0fcf553c06363dfef86d74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.powerlevels.Role + +/** + * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. + */ +@JsonClass(generateAdapter = true) +data class PowerLevelsContent( + @Json(name = "ban") val ban: Int = Role.Moderator.value, + @Json(name = "kick") val kick: Int = Role.Moderator.value, + @Json(name = "invite") val invite: Int = Role.Moderator.value, + @Json(name = "redact") val redact: Int = Role.Moderator.value, + @Json(name = "events_default") val eventsDefault: Int = Role.Default.value, + @Json(name = "events") val events: MutableMap<String, Int> = HashMap(), + @Json(name = "users_default") val usersDefault: Int = Role.Default.value, + @Json(name = "users") val users: MutableMap<String, Int> = HashMap(), + @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, + @Json(name = "notifications") val notifications: Map<String, Any> = HashMap() +) { + /** + * Alter this content with a new power level for the specified user + * + * @param userId the userId to alter the power level of + * @param powerLevel the new power level, or null to set the default value. + */ + fun setUserPowerLevel(userId: String, powerLevel: Int?) { + if (powerLevel == null || powerLevel == usersDefault) { + users.remove(userId) + } else { + users[userId] = powerLevel + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..97fd0a16abb963c64810d0179fb77febf562f405 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +data class ReactionAggregatedSummary( + val key: String, // "ðŸ‘" + val count: Int, // 8 + val addedByMe: Boolean, // true + val firstTimestamp: Long, // unix timestamp + val sourceEvents: List<String>, + val localEchoEvents: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6ced198d7d42b5b3e89b1645f962c208d4fbb0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.user.model.User + +data class ReadReceipt( + val user: User, + val originServerTs: Long +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..82291fa0625b906ca98087fe705ec8102f9da057 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.room.VerificationState + +/** + * Contains an aggregated summary info of the references. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class ReferencesAggregatedContent( + // Verification status info for m.key.verification.request msgType events + @Json(name = "verif_sum") val verificationState: VerificationState + // Add more fields for future summary info. +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..298567262a8fc63052e093a4ca39b5c3b03ea3bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * Events can relates to other events, this object keeps a summary + * of all events that are referencing the 'eventId' event via the RelationType.REFERENCE + */ +data class ReferencesAggregatedSummary( + val eventId: String, + val content: Content?, + val sourceEvents: List<String>, + val localEchos: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..94628e69872b91f4dd490414fe6300a592e5d849 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_ALIASES state event content + */ +@JsonClass(generateAdapter = true) +data class RoomAliasesContent( + @Json(name = "aliases") val aliases: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..ded2e49657d414e7d080930cdbed5ef65f87fc86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_AVATAR state event content + */ +@JsonClass(generateAdapter = true) +data class RoomAvatarContent( + @Json(name = "url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..d5f41b66dc2887ba0b540fa12381c430be41e2b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content + */ +@JsonClass(generateAdapter = true) +data class RoomCanonicalAliasContent( + @Json(name = "alias") val canonicalAlias: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd3788440772149b2f864a3ffcc544ff4bc6a1f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class RoomDirectoryVisibility { + @Json(name = "private") PRIVATE, + @Json(name = "public") PUBLIC +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2b944c0eb179c1565add50708cd5338e79bd6f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_GUEST_ACCESS state event content + * Ref: https://matrix.org/docs/spec/client_server/latest#m-room-guest-access + */ +@JsonClass(generateAdapter = true) +data class RoomGuestAccessContent( + // Required. Whether guests can join the room. One of: ["can_join", "forbidden"] + @Json(name = "guest_access") val guestAccess: GuestAccess? = null +) + +@JsonClass(generateAdapter = false) +enum class GuestAccess(val value: String) { + @Json(name = "can_join") + CanJoin("can_join"), + @Json(name = "forbidden") + Forbidden("forbidden") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6546b10652cda24253cdd6222682b82dbe7a260 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility + */ +@JsonClass(generateAdapter = false) +enum class RoomHistoryVisibility { + /** + * All events while this is the m.room.history_visibility value may be shared by any + * participating homeserver with anyone, regardless of whether they have ever joined the room. + */ + @Json(name = "world_readable") WORLD_READABLE, + /** + * Previous events are always accessible to newly joined members. All events in the + * room are accessible, even those sent when the member was not a part of the room. + */ + @Json(name = "shared") SHARED, + /** + * Events are accessible to newly joined members from the point they were invited onwards. + * Events stop being accessible when the member's state changes to something other than invite or join. + */ + @Json(name = "invited") INVITED, + /** + * Events are accessible to newly joined members from the point they joined the room onwards. + * Events stop being accessible when the member's state changes to something other than join. + */ + @Json(name = "joined") JOINED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8955320d893fe1bf86eeeca75bf928279f33c4fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomHistoryVisibilityContent( + @Json(name = "history_visibility") val historyVisibility: RoomHistoryVisibility? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6c947b753f0a94b538d17e1f4099c6a8fc1c3de --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules + */ +@JsonClass(generateAdapter = false) +enum class RoomJoinRules(val value: String) { + + @Json(name = "public") + PUBLIC("public"), + + @Json(name = "invite") + INVITE("invite"), + + @Json(name = "knock") + KNOCK("knock"), + + @Json(name = "private") + PRIVATE("private") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..14a88885b6cbba543bc004e13cb2ec428b758ad0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_JOIN_RULES state event content + */ +@JsonClass(generateAdapter = true) +data class RoomJoinRulesContent( + @Json(name = "join_rule") val joinRules: RoomJoinRules? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..278db67a0e0a36398704feeabc3b4ecba0fd2fe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.UnsignedData + +/** + * Class representing the EventType.STATE_ROOM_MEMBER state event content + */ +@JsonClass(generateAdapter = true) +data class RoomMemberContent( + @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, + @Json(name = "displayname") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null, + @Json(name = "is_direct") val isDirect: Boolean = false, + @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..17b0cf30b1b0c56b9c71117685f02c892f49f22f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +/** + * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content + */ +data class RoomMemberSummary constructor( + val membership: Membership, + val userId: String, + val displayName: String? = null, + val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3d93a5a16a1790eee858a724085c9753b1daafe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_NAME state event content + */ +@JsonClass(generateAdapter = true) +data class RoomNameContent( + @Json(name = "name") val name: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..0df86e09d7651658f0f65cff6018bc6440e264cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This class holds some data of a room. + * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class RoomSummary constructor( + val roomId: String, + // Computed display name + val displayName: String = "", + val name: String = "", + val topic: String = "", + val avatarUrl: String = "", + val canonicalAlias: String? = null, + val aliases: List<String> = emptyList(), + val isDirect: Boolean = false, + val joinedMembersCount: Int? = 0, + val invitedMembersCount: Int? = 0, + val latestPreviewableEvent: TimelineEvent? = null, + val otherMemberIds: List<String> = emptyList(), + val notificationCount: Int = 0, + val highlightCount: Int = 0, + val hasUnreadMessages: Boolean = false, + val tags: List<RoomTag> = emptyList(), + val membership: Membership = Membership.NONE, + val versioningState: VersioningState = VersioningState.NONE, + val readMarkerId: String? = null, + val userDrafts: List<UserDraft> = emptyList(), + val isEncrypted: Boolean, + val encryptionEventTs: Long?, + val typingUsers: List<SenderInfo>, + val inviterId: String? = null, + val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, + val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, + val hasFailedSending: Boolean = false +) { + + val isVersioned: Boolean + get() = versioningState != VersioningState.NONE + + val hasNewMessages: Boolean + get() = notificationCount != 0 + + val isFavorite: Boolean + get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } + + val canStartCall: Boolean + get() = joinedMembersCount == 2 + + companion object { + const val NOT_IN_BREADCRUMBS = -1 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8372f31423c4765920d256d854f0565ccc07fa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite + */ +@JsonClass(generateAdapter = true) +data class RoomThirdPartyInviteContent( + /** + * Required. A user-readable string which represents the user who has been invited. + * This should not contain the user's third party ID, as otherwise when the invite + * is accepted it would leak the association between the matrix ID and the third party ID. + */ + @Json(name = "display_name") val displayName: String, + + /** + * Required. A URL which can be fetched, with querystring public_key=public_key, to validate + * whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. + */ + @Json(name = "key_validity_url") val keyValidityUrl: String, + + /** + * Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in + * public_keys is also sufficient). This exists for backwards compatibility. + */ + @Json(name = "public_key") val publicKey: String, + + /** + * Keys with which the token may be signed. + */ + @Json(name = "public_keys") val publicKeys: List<PublicKeys> = emptyList() +) + +@JsonClass(generateAdapter = true) +data class PublicKeys( + /** + * An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key + * has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL + * is absent, the key must be considered valid indefinitely. + */ + @Json(name = "key_validity_url") val keyValidityUrl: String? = null, + + /** + * Required. A base-64 encoded ed25519 key with which token may be signed. + */ + @Json(name = "public_key") val publicKey: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..38d3ca93f4abfe510fdeea211838750d51b4d8cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_TOPIC state event content + */ +@JsonClass(generateAdapter = true) +data class RoomTopicContent( + @Json(name = "topic") val topic: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c14275b3ee7fc2c8293d197052ef4d75e68df41 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json + +data class Signed( + @Json(name = "token") val token: String, + @Json(name = "signatures") val signatures: Any, + @Json(name = "mxid") val mxid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt new file mode 100644 index 0000000000000000000000000000000000000000..202d64d621d4f81b00259dc6d394e8f8d68fc80e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +enum class VersioningState { + NONE, + UPGRADED_ROOM_NOT_JOINED, + UPGRADED_ROOM_JOINED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e21ebea86ccf19ffb9f57ac4a6559f79673c6a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the callee when they wish to answer the call. + */ +@JsonClass(generateAdapter = true) +data class CallAnswerContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. The session description object + */ + @Json(name = "answer") val answer: Answer, + /** + * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0 +) { + + @JsonClass(generateAdapter = true) + data class Answer( + /** + * Required. The type of session description. Must be 'answer'. + */ + @Json(name = "type") val type: SdpType = SdpType.ANSWER, + /** + * Required. The SDP text of the session description. + */ + @Json(name = "sdp") val sdp: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a6f1c82c320a830cbcb2f9e0f5fe7dd24ba651c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by callers after sending an invite and by the callee after answering. + * Its purpose is to give the other party additional ICE candidates to try using to communicate. + */ +@JsonClass(generateAdapter = true) +data class CallCandidatesContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. Array of objects describing the candidates. + */ + @Json(name = "candidates") val candidates: List<Candidate> = emptyList(), + /** + * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0 +) { + + @JsonClass(generateAdapter = true) + data class Candidate( + /** + * Required. The SDP media type this candidate is intended for. + */ + @Json(name = "sdpMid") val sdpMid: String, + /** + * Required. The index of the SDP 'm' line this candidate is intended for. + */ + @Json(name = "sdpMLineIndex") val sdpMLineIndex: Int, + /** + * Required. The SDP 'a' line of the candidate. + */ + @Json(name = "candidate") val candidate: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..aef774008c8344d7d7910342d8e313dbc046e083 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Sent by either party to signal their termination of the call. This can be sent either once + * the call has been established or before to abort the call. + */ +@JsonClass(generateAdapter = true) +data class CallHangupContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0, + /** + * Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call. + * When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails + * or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"] + */ + @Json(name = "reason") val reason: Reason? = null +) { + @JsonClass(generateAdapter = false) + enum class Reason { + @Json(name = "ice_failed") + ICE_FAILED, + + @Json(name = "invite_timeout") + INVITE_TIMEOUT + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..6baef034c2092c063080f35d536526eebd844392 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the caller when they wish to establish a call. + */ +@JsonClass(generateAdapter = true) +data class CallInviteContent( + /** + * Required. A unique identifier for the call. + */ + @Json(name = "call_id") val callId: String?, + /** + * Required. The session description object + */ + @Json(name = "offer") val offer: Offer?, + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int? = 0, + /** + * Required. The time in milliseconds that the invite is valid for. + * Once the invite age exceeds this value, clients should discard it. + * They should also no longer show the call as awaiting an answer in the UI. + */ + @Json(name = "lifetime") val lifetime: Int? +) { + @JsonClass(generateAdapter = true) + data class Offer( + /** + * Required. The type of session description. Must be 'offer'. + */ + @Json(name = "type") val type: SdpType? = SdpType.OFFER, + /** + * Required. The SDP text of the session description. + */ + @Json(name = "sdp") val sdp: String? + ) { + companion object { + const val SDP_VIDEO = "m=video" + } + } + + fun isVideo() = offer?.sdp?.contains(Offer.SDP_VIDEO) == true +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt new file mode 100644 index 0000000000000000000000000000000000000000..a760e6ef93e84fc88e65862a28675cc2d05e670c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class SdpType { + @Json(name = "offer") + OFFER, + + @Json(name = "answer") + ANSWER +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e78ae10f9ca3e85c6a6e63f3ab726b32acd4877 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +// TODO Give a way to include other initial states +class CreateRoomParams { + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + var visibility: RoomDirectoryVisibility? = null + + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + var roomAliasName: String? = null + + /** + * If this is not null, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + var name: String? = null + + /** + * If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + var topic: String? = null + + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + val invitedUserIds = mutableListOf<String>() + + /** + * A list of objects representing third party IDs to invite into the room. + */ + val invite3pids = mutableListOf<ThreePid>() + + /** + * If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users, + * the encryption will be enabled on the created room + */ + var enableEncryptionIfInvitedUsersSupportIt: Boolean = false + + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. + */ + var preset: CreateRoomPreset? = null + + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + var isDirect: Boolean? = null + + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + var creationContent: Any? = null + + /** + * The power level content to override in the default power level event + */ + var powerLevelContentOverride: PowerLevelsContent? = null + + /** + * Mark as a direct message room. + */ + fun setDirectMessage() { + preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT + isDirect = true + } + + /** + * Supported value: MXCRYPTO_ALGORITHM_MEGOLM + */ + var algorithm: String? = null + private set + + var historyVisibility: RoomHistoryVisibility? = null + + fun enableEncryption() { + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt new file mode 100644 index 0000000000000000000000000000000000000000..7bc4f664c5fee218ac617f6233fb9aa3f54d96e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class CreateRoomPreset { + @Json(name = "private_chat") + PRESET_PRIVATE_CHAT, + + @Json(name = "public_chat") + PRESET_PUBLIC_CHAT, + + @Json(name = "trusted_private_chat") + PRESET_TRUSTED_PRIVATE_CHAT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..fb726808e915a082bdb4cc6cbd4fa89c5f2c54a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * A link to an old room in case of room versioning + */ +@JsonClass(generateAdapter = true) +data class Predecessor( + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a9b4ca9cf4ddcb7ac6eded766b4227bb3c0e8bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Content of a m.room.create type event + */ +@JsonClass(generateAdapter = true) +data class RoomCreateContent( + @Json(name = "creator") val creator: String? = null, + @Json(name = "room_version") val roomVersion: String? = null, + @Json(name = "predecessor") val predecessor: Predecessor? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b0097db2cf08691ace1ed82c0f7d93c1cf17d8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AudioInfo( + /** + * The mimetype of the audio e.g. "audio/aac". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The size of the audio clip in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The duration of the audio in milliseconds. + */ + @Json(name = "duration") val duration: Int = 0 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..290909ded1f997a0d2210e25ce3808adb7096f21 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class FileInfo( + /** + * The mimetype of the file e.g. application/msword. + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The size of the file in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..26c196bb140a51c9ce454649003c1d38d586a10b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class ImageInfo( + /** + * The mimetype of the image, e.g. "image/jpeg". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "h") val height: Int = 0, + + /** + * Size of the image in bytes. + */ + @Json(name = "size") val size: Int = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL (typically MXC URI) to a thumbnail of the image. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..258fd75c9416a272fccecaa447a7d314244450ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class LocationInfo( + /** + * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..14022075c2a7a6a9d274144bffe42ea451307aa1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageAudioContent( + /** + * Required. Must be 'm.audio'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata for the audio clip referred to in url. + */ + @Json(name = "info") val audioInfo: AudioInfo? = null, + + /** + * Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: audioInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8b5c98d2500219ce279f930e3958ec9a9ddb23b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +interface MessageContent { + val msgType: String + val body: String + val relatesTo: RelationDefaultContent? + val newContent: Content? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..15609dca3baa497639a216f8ba6fb53a5518a816 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +interface MessageContentWithFormattedBody : MessageContent { + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + val format: String? + + /** + * The formatted version of the body. This is required if format is specified. + */ + val formattedBody: String? + + /** + * Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html" + */ + val matrixFormattedBody: String? + get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b033755bdf4a8c9aa2cec2cb337a280dedcb15a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageDefaultContent( + @Json(name = "msgtype") override val msgType: String, + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..36ec85ebf009076425c6f05d1d59def7064e1f38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageEmoteContent( + /** + * Required. Must be 'm.emote'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The emote action to perform. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..bbdb2835b1f0b6f3dc6b193542d840c004a179df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import android.webkit.MimeTypeMap +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageFileContent( + /** + * Required. Must be 'm.file'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A human-readable description of the file. This is recommended to be the filename of the original upload. + */ + @Json(name = "body") override val body: String, + + /** + * The original filename of the uploaded file. + */ + @Json(name = "filename") val filename: String? = null, + + /** + * Information about the file referred to in url. + */ + @Json(name = "info") val info: FileInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the file. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + + override val mimeType: String? + get() = encryptedFileInfo?.mimetype + ?: info?.mimeType + ?: MimeTypeMap.getFileExtensionFromUrl(filename ?: body)?.let { extension -> + MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + + fun getFileName(): String { + return filename ?: body + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt new file mode 100644 index 0000000000000000000000000000000000000000..c32b0586ea476faf5eae1ff26f9e223f1cd1be44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +object MessageFormat { + const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..48e30508bc6a5052a272e08ed4f1f5e214b16a40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageImageContent( + /** + * Required. Must be 'm.image'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, + * or some kind of content description for accessibility e.g. 'image attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the image referred to in url. + */ + @Json(name = "info") override val info: ImageInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..e14d531a4f4e0968af8251d828ea3d72279594e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.model.message + +/** + * A content with image information + */ +interface MessageImageInfoContent : MessageWithAttachmentContent { + val info: ImageInfo? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..3452e291eb66a9bf1c05cc28029014358d3c6c70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageLocationContent( + /** + * Required. Must be 'm.location'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Required. A geo URI representing this location. + */ + @Json(name = "geo_uri") val geoUri: String, + + /** + * + */ + @Json(name = "info") val locationInfo: LocationInfo? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c2dd2a1965f9932c283130df8bcbfa3fc3d12da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageNoticeContent( + /** + * Required. Must be 'm.notice'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The notice text to send. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..caaf5151af13577c33f5b09ada4715c096ffecdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +// Possible values for optionType +const val OPTION_TYPE_POLL = "org.matrix.poll" +const val OPTION_TYPE_BUTTONS = "org.matrix.buttons" + +/** + * Polls and bot buttons are m.room.message events with a msgtype of m.options, + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessageOptionsContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_OPTIONS, + @Json(name = "type") val optionType: String? = null, + @Json(name = "body") override val body: String, + @Json(name = "label") val label: String?, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "options") val options: List<OptionItem>? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7dd0160cbd5aa81847bb454be4bd094ddcd467d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessagePollResponseContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_RESPONSE, + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..a97c0f86e35045a755b9c4e945e77aed1e90481b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageRelationContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..fad04941f7f8887589309f000a8b4e73c7a6a9d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageStickerContent( + /** + * Set in local, not from server + */ + override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL, + + /** + * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, + * or some kind of content description for accessibility e.g. 'image attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the image referred to in url. + */ + @Json(name = "info") override val info: ImageInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea8685ae71ff119c197580dfc7b24e183ad3caa7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageTextContent( + /** + * Required. Must be 'm.text'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The body of the message. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt new file mode 100644 index 0000000000000000000000000000000000000000..026132b7c52f398688f2f882320536ca0b2168a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +object MessageType { + const val MSGTYPE_TEXT = "m.text" + const val MSGTYPE_EMOTE = "m.emote" + const val MSGTYPE_NOTICE = "m.notice" + const val MSGTYPE_IMAGE = "m.image" + const val MSGTYPE_AUDIO = "m.audio" + const val MSGTYPE_VIDEO = "m.video" + const val MSGTYPE_LOCATION = "m.location" + const val MSGTYPE_FILE = "m.file" + const val MSGTYPE_OPTIONS = "org.matrix.options" + const val MSGTYPE_RESPONSE = "org.matrix.response" + const val MSGTYPE_POLL_CLOSED = "org.matrix.poll_closed" + const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" + // Add, in local, a fake message type in order to StickerMessage can inherit Message class + // Because sticker isn't a message type but a event type without msgtype field + const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7a8a4a6f87940e61225cad666f1b8202c40b95e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationAcceptContent( + @Json(name = "hash") override val hash: String?, + @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, + @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "commitment") override var commitment: String? = null +) : VerificationInfoAccept { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoAcceptFactory { + + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>): VerificationInfoAccept { + return MessageVerificationAcceptContent( + hash, + keyAgreementProtocol, + messageAuthenticationCode, + shortAuthenticationStrings, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ), + commitment + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..944599a15343c9203d66a36c079e8343e2b1d7c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel + +@JsonClass(generateAdapter = true) +data class MessageVerificationCancelContent( + @Json(name = "code") override val code: String? = null, + @Json(name = "reason") override val reason: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoCancel { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object { + fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { + return MessageVerificationCancelContent( + reason.value, + reason.humanReadable, + RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..13593b60b8eac8bfaa8a07db4e7cb6296a5763b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationDoneContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfo<ValidVerificationDone> { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent(): Content? = toContent() + + override fun asValidObject(): ValidVerificationDone? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationDone( + validTransactionId + ) + } +} + +internal data class ValidVerificationDone( + val transactionId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..00d4e2cd0b9d746c22521596c6c96bdfe5335b12 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationKeyContent( + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoKey { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoKeyFactory { + + override fun create(tid: String, pubKey: String): VerificationInfoKey { + return MessageVerificationKeyContent( + pubKey, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ac43e49d090d7d6a6e0ed141b9fd165a012112b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationMacContent( + @Json(name = "mac") override val mac: Map<String, String>? = null, + @Json(name = "keys") override val keys: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoMac { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac { + return MessageVerificationMacContent( + mac, + keys, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..eafdb30ecf884bfb3877f933c83c26381358f2d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationReadyContent( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "methods") override val methods: List<String>? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoReady { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : MessageVerificationReadyFactory { + override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + methods = methods, + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..b89ff075524c1eccefb1612e1e6f7cdaf37dfb3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest + +@JsonClass(generateAdapter = true) +data class MessageVerificationRequestContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = "body") override val body: String, + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List<String>, + @Json(name = "to") val toUserId: String, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "format") val format: String? = null, + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + // Not parsed, but set after, using the eventId + override val transactionId: String? = null +) : MessageContent, VerificationInfoRequest { + + override fun toEventContent() = toContent() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..c6c5cb6208a8c9fea4c8c5643111ee43d8a04399 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationStartContent( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "hashes") override val hashes: List<String>?, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>?, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?, + @Json(name = "method") override val method: String?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "secret") override val sharedSecret: String? +) : VerificationInfoStart { + + override fun toCanonicalJson(): String { + return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) + } + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..02456e6b0ffb32ef75c3454e0994f279f5aad66e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageVideoContent( + /** + * Required. Must be 'm.video'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the video clip referred to in url. + */ + @Json(name = "info") val videoInfo: VideoInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the video clip. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: videoInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..af07d37efde50429d92015e91c8f3dd5854bec28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +/** + * Interface for message which can contains an encrypted file + */ +interface MessageWithAttachmentContent : MessageContent { + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + val url: String? + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + val encryptedFileInfo: EncryptedFileInfo? + + val mimeType: String? +} + +/** + * Get the url of the encrypted file or of the file + */ +fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url + +fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: body diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6e797904092dc44b1c77077f6500e72bba16e32 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class OptionItem( + @Json(name = "label") val label: String?, + @Json(name = "value") val value: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..bedbe526419c34ec9eb73428654e16ff443780a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThumbnailInfo( + /** + * The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "h") val height: Int = 0, + + /** + * Size of the image in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The mimetype of the image, e.g. "image/jpeg". + */ + @Json(name = "mimetype") val mimeType: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..b16c3dd82303133d3afc960b75ef95fe8c46fdc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class VideoInfo( + /** + * The mimetype of the video e.g. "video/mp4". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The width of the video in pixels. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The height of the video in pixels. + */ + @Json(name = "h") val height: Int = 0, + + /** + * The size of the video in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The duration of the video in milliseconds. + */ + @Json(name = "duration") val duration: Int = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL (typically MXC URI) to an image thumbnail of the video clip. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f2b2c7facbeedf18c765b15338c794cd819e1dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionContent( + @Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..97577c90d12b893cf3d6592db835b31e024f2e7d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionInfo( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String, + val key: String, + // always null for reaction + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3822118c0cb6c5a2b70ac1d9c94cf0f868a5aea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import org.matrix.android.sdk.api.session.events.model.RelationType + +interface RelationContent { + /** See [RelationType] for known possible values */ + val type: String? + val eventId: String? + val inReplyTo: ReplyToContent? + val option: Int? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..eedc23518f7dcb533e417c12211b45b1e4f7888a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RelationDefaultContent( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String?, + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3d739fc62ce743758baecedb2cd61617151d5db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.relation + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * In some cases, events may wish to reference other events. + * This could be to form a thread of messages for the user to follow along with, + * or to provide more context as to what a particular event is describing. + * Relation are used to associate new information with an existing event. + * + * Relations are events which have an m.relates_to mixin in their contents, + * and the new information they convey is expressed in their usual event type and content. + * + * Three types of relations are defined, each defining different behaviour when aggregated: + * + * m.annotation - lets you define an event which annotates an existing event. + * When aggregated, groups events together based on key and returns a count. + * (aka SQL's COUNT) These are primarily intended for handling reactions. + * + * m.replace - lets you define an event which replaces an existing event. + * When aggregated, returns the most recent replacement event. (aka SQL's MAX) + * These are primarily intended for handling edits. + * + * m.reference - lets you define an event which references an existing event. + * When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads). + * These are primarily intended for handling replies (and in future threads). + */ +interface RelationService { + + /** + * Sends a reaction (emoji) to the targetedEvent. + * It has no effect if the user has already added the same reaction to the event. + * @param targetEventId the id of the event being reacted + * @param reaction the reaction (preferably emoji) + */ + fun sendReaction(targetEventId: String, + reaction: String): Cancelable + + /** + * Undo a reaction (emoji) to the targetedEvent. + * @param targetEventId the id of the event being reacted + * @param reaction the reaction (preferably emoji) + */ + fun undoReaction(targetEventId: String, + reaction: String): Cancelable + + /** + * Edit a text message body. Limited to "m.text" contentType + * @param targetEventId The event to edit + * @param newBodyText The edited body + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editTextMessage(targetEventId: String, + msgType: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + compatibilityBodyText: String = "* $newBodyText"): Cancelable + + /** + * Edit a reply. This is a special case because replies contains fallback text as a prefix. + * This method will take the new body (stripped from fallbacks) and re-add them before sending. + * @param replyToEdit The event to edit + * @param originalTimelineEvent the message that this reply (being edited) is relating to + * @param newBodyText The edited body (stripped from in reply to content) + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editReply(replyToEdit: TimelineEvent, + originalTimelineEvent: TimelineEvent, + newBodyText: String, + compatibilityBodyText: String = "* $newBodyText"): Cancelable + + /** + * Get the edit history of the given event + */ + fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) + + /** + * Reply to an event in the timeline (must be in same room) + * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + * The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated + * by the sdk into pills. + * @param eventReplied the event referenced by the reply + * @param replyText the reply text + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + */ + fun replyToMessage(eventReplied: TimelineEvent, + replyText: CharSequence, + autoMarkdown: Boolean = false): Cancelable? + + /** + * Get the current EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the EventAnnotationsSummary found + */ + fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? + + /** + * Get a LiveData of EventAnnotationsSummary for the specified eventId + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the LiveData of EventAnnotationsSummary + */ + fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..57d8adb6d66d93c26459ad24064b5855e4416815 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReplyToContent( + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt new file mode 100644 index 0000000000000000000000000000000000000000..0429eba1b001595cf274443a7896cc50dbdac38f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the objects returned by /publicRooms call. + */ +@JsonClass(generateAdapter = true) +data class PublicRoom( + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List<String>? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false +) { + /** + * Return the canonical alias, or the first alias from the list of aliases, or null + */ + fun getPrimaryAlias(): String? { + return canonicalAlias ?: aliases?.firstOrNull() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..041b804576a724750621ec33be2045ac82a94d96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to define a filter to retrieve public rooms + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsFilter( + /** + * A string to search for in the room metadata, e.g. name, topic, canonical alias etc. (Optional). + */ + @Json(name = "generic_search_term") + val searchTerm: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..c69cde72b7314b53b774f21f3d30641799746c97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to get the public rooms list + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsParams( + /** + * Limit the number of results returned. + */ + @Json(name = "limit") + val limit: Int? = null, + + /** + * A pagination token from a previous request, allowing clients to get the next (or previous) batch of rooms. + * The direction of pagination is specified solely by which token is supplied, rather than via an explicit flag. + */ + @Json(name = "since") + val since: String? = null, + + /** + * Filter to apply to the results. + */ + @Json(name = "filter") + val filter: PublicRoomsFilter? = null, + + /** + * Whether or not to include all known networks/protocols from application services on the homeserver. Defaults to false. + */ + @Json(name = "include_all_networks") + val includeAllNetworks: Boolean = false, + + /** + * The specific third party network/protocol to request from the homeserver. Can only be used if include_all_networks is false. + */ + @Json(name = "third_party_instance_id") + val thirdPartyInstanceId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..39f24d658cc6c17506022c7db7c97c935a387ceb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the public rooms request response + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsResponse( + /** + * A pagination token for the response. The absence of this token means there are no more results to fetch and the client should stop paginating. + */ + @Json(name = "next_batch") + val nextBatch: String? = null, + + /** + * A pagination token that allows fetching previous results. The absence of this token means there are no results before this batch, + * i.e. this is the first batch. + */ + @Json(name = "prev_batch") + val prevBatch: String? = null, + + /** + * A paginated chunk of public rooms. + */ + @Json(name = "chunk") + val chunk: List<PublicRoom>? = null, + + /** + * An estimate on the total number of public rooms, if the server has an estimate. + */ + @Json(name = "total_room_count_estimate") + val totalRoomCountEstimate: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5303773bc193e07d9c79390d2373960412b1bca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.tag + +data class RoomTag( + val name: String, + val order: Double? +) { + + companion object { + const val ROOM_TAG_FAVOURITE = "m.favourite" + const val ROOM_TAG_LOW_PRIORITY = "m.lowpriority" + const val ROOM_TAG_SERVER_NOTICE = "m.server_notice" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..15b83a4af1d68fd03b66db61388af0fb2cf5e1e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.tag + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomTagContent( + @Json(name = "tags") val tags: Map<String, Map<String, Any>> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt new file mode 100644 index 0000000000000000000000000000000000000000..36bc9496066669116734d5c1f6d04ca8788f9186 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FieldType( + /** + * Required. A regular expression for validation of a field's value. This may be relatively coarse to verify the value as the application + * service providing this protocol may apply additional + */ + @Json(name = "regexp") + val regexp: String? = null, + + /** + * Required. An placeholder serving as a valid example of the field value. + */ + @Json(name = "placeholder") + val placeholder: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt new file mode 100644 index 0000000000000000000000000000000000000000..e2cdd25b7c9b0d36ba27dff4b5df764a4a15cdfe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +/** + * This class describes a rooms directory server. + */ +data class RoomDirectoryData( + + /** + * The server name (might be null) + * Set null when the server is the current user's home server. + */ + val homeServer: String? = null, + + /** + * The display name (the server description) + */ + val displayName: String = DEFAULT_HOME_SERVER_NAME, + + /** + * The third party server identifier + */ + val thirdPartyInstanceId: String? = null, + + /** + * Tell if all the federated servers must be included + */ + val includeAllNetworks: Boolean = false, + + /** + * the avatar url + */ + val avatarUrl: String? = null +) { + + companion object { + const val DEFAULT_HOME_SERVER_NAME = "Matrix" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt new file mode 100644 index 0000000000000000000000000000000000000000..ebc147e69ff7c09304933389177746a6d183e3f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocol( + /** + * Required. Fields which may be used to identify a third party user. These should be ordered to suggest the way that entities may be grouped, + * where higher groupings are ordered first. For example, the name of a network should be searched before the nickname of a user. + */ + @Json(name = "user_fields") + val userFields: List<String>? = null, + + /** + * Required. Fields which may be used to identify a third party location. These should be ordered to suggest the way that + * entities may be grouped, where higher groupings are ordered first. For example, the name of a network should be + * searched before the name of a channel. + */ + @Json(name = "location_fields") + val locationFields: List<String>? = null, + + /** + * Required. A content URI representing an icon for the third party protocol. + * + * FIXDOC: This field was not present in legacy Riot, and it is sometimes sent by the server (so not Required?) + */ + @Json(name = "icon") + val icon: String? = null, + + /** + * Required. The type definitions for the fields defined in the user_fields and location_fields. Each entry in those arrays MUST have an entry here. + * The string key for this object is field name itself. + * + * May be an empty object if no fields are defined. + */ + @Json(name = "field_types") + val fieldTypes: Map<String, FieldType>? = null, + + /** + * Required. A list of objects representing independent instances of configuration. For example, multiple networks on IRC + * if multiple are provided by the same application service. + */ + @Json(name = "instances") + val instances: List<ThirdPartyProtocolInstance>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt new file mode 100644 index 0000000000000000000000000000000000000000..04e5481259d42364a75e689072f2b579aa2017a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocolInstance( + /** + * Required. A human-readable description for the protocol, such as the name. + */ + @Json(name = "desc") + val desc: String? = null, + + /** + * An optional content URI representing the protocol. Overrides the one provided at the higher level Protocol object. + */ + @Json(name = "icon") + val icon: String? = null, + + /** + * Required. Preset values for fields the client may use to search by. + */ + @Json(name = "fields") + val fields: Map<String, Any>? = null, + + /** + * Required. A unique identifier across all instances. + */ + @Json(name = "network_id") + val networkId: String? = null, + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "instance_id") + val instanceId: String? = null, + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "bot_user_id") + val botUserId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..43b56c8b9d11731f43e4a8daab46b2772bc49224 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.room.model.tombstone + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to contains Tombstone information + */ +@JsonClass(generateAdapter = true) +data class RoomTombstoneContent( + /** + * Required. A server-defined message. + */ + @Json(name = "body") val body: String? = null, + + /** + * Required. The new room the client should be visiting. + */ + @Json(name = "replacement_room") val replacementRoomId: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt new file mode 100644 index 0000000000000000000000000000000000000000..42971e874a6e6381f59a765794ce262419e02d49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.notification + +/** + * Defines the room notification state + */ +enum class RoomNotificationState { + /** + * All the messages will trigger a noisy notification + */ + ALL_MESSAGES_NOISY, + + /** + * All the messages will trigger a notification + */ + ALL_MESSAGES, + + /** + * Only the messages with user display name / user name will trigger notifications + */ + MENTIONS_ONLY, + + /** + * No notifications + */ + MUTE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt new file mode 100644 index 0000000000000000000000000000000000000000..79070adea35bcd044aa9f20847e623cb2ec88aab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.notification + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface RoomPushRuleService { + + fun getLiveRoomNotificationState(): LiveData<RoomNotificationState> + + fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..34e3168ce1fec3b44cda1a571dc8de860fae22a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.powerlevels + +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent + +/** + * This class is an helper around PowerLevelsContent. + */ +class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserPowerLevelValue(userId: String): Int { + return powerLevelsContent.users.getOrElse(userId) { + powerLevelsContent.usersDefault + } + } + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserRole(userId: String): Role { + val value = getUserPowerLevelValue(userId) + // I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web + return Role.fromValue(value, powerLevelsContent.eventsDefault) + } + + /** + * Tell if an user can send an event of a certain type + * + * @param userId the id of the user to check for. + * @param isState true if the event is a state event (ie. state key is not null) + * @param eventType the event type to check for + * @return true if the user can send this type of event + */ + fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean { + return if (userId.isNotEmpty()) { + val powerLevel = getUserPowerLevelValue(userId) + val minimumPowerLevel = powerLevelsContent.events[eventType] + ?: if (isState) { + powerLevelsContent.stateDefault + } else { + powerLevelsContent.eventsDefault + } + powerLevel >= minimumPowerLevel + } else false + } + + /** + * Check if the user have the necessary power level to invite + * @param userId the id of the user to check for. + * @return true if able to invite + */ + fun isUserAbleToInvite(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.invite + } + + /** + * Check if the user have the necessary power level to ban + * @param userId the id of the user to check for. + * @return true if able to ban + */ + fun isUserAbleToBan(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.ban + } + + /** + * Check if the user have the necessary power level to kick + * @param userId the id of the user to check for. + * @return true if able to kick + */ + fun isUserAbleToKick(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.kick + } + + /** + * Check if the user have the necessary power level to redact + * @param userId the id of the user to check for. + * @return true if able to redact + */ + fun isUserAbleToRedact(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.redact + } + + /** + * Get the notification level for a dedicated key. + * + * @param key the notification key + * @return the level + */ + fun notificationLevel(key: String): Int { + return when (val value = powerLevelsContent.notifications[key]) { + // the first implementation was a string value + is String -> value.toInt() + is Int -> value + else -> Role.Moderator.value + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ac479786e405e91b44886d35163814f2eaf2593 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.api.session.room.powerlevels + +import androidx.annotation.StringRes +import org.matrix.android.sdk.R + +sealed class Role(open val value: Int, @StringRes val res: Int) : Comparable<Role> { + object Admin : Role(100, R.string.power_level_admin) + object Moderator : Role(50, R.string.power_level_moderator) + object Default : Role(0, R.string.power_level_default) + data class Custom(override val value: Int) : Role(value, R.string.power_level_custom) + + override fun compareTo(other: Role): Int { + return value.compareTo(other.value) + } + + companion object { + + // Order matters, default value should be checked after defined roles + fun fromValue(value: Int, default: Int): Role { + return when (value) { + Admin.value -> Admin + Moderator.value -> Moderator + Default.value, + default -> Default + else -> Custom(value) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt new file mode 100644 index 0000000000000000000000000000000000000000..3aa9d60e6a9c8db9d3f2ba9b34042cd517a53452 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.read + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. + */ +interface ReadService { + + enum class MarkAsReadParams { + READ_RECEIPT, + READ_MARKER, + BOTH + } + + /** + * Force the read marker to be set on the latest event. + */ + fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback<Unit>) + + /** + * Set the read receipt on the event with provided eventId. + */ + fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) + + /** + * Set the read marker on the event with provided eventId. + */ + fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) + + /** + * Check if an event is already read, ie. your read receipt is set on a more recent event. + */ + fun isEventRead(eventId: String): Boolean + + /** + * Returns a live read marker id for the room. + */ + fun getReadMarkerLive(): LiveData<Optional<String>> + + /** + * Returns a live read receipt id for the room. + */ + fun getMyReadReceiptLive(): LiveData<Optional<String>> + + /** + * Returns a live list of read receipts for a given event + * @param eventId: the event + */ + fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..42a21fab902ff91cf774553b9c6919c8445cca92 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.reporting + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to report content of an event. + */ +interface ReportingService { + + /** + * Report content + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid + */ + fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc91d5177f1fd42098f46312051dfa269fa6f344 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.send + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface DraftService { + + /** + * Save or update a draft to the room + */ + fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable + + /** + * Delete the last draft, basically just after sending the message + */ + fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable + + /** + * Return the current drafts if any, as a live data + * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts + */ + fun getDraftsLive(): LiveData<List<UserDraft>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt new file mode 100644 index 0000000000000000000000000000000000000000..a96339a111f1dc463c34ed7a6da8f3acb4890b98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.send + +import org.matrix.android.sdk.api.util.MatrixItem + +/** + * Tag class for spans that should mention a matrix item. + * These Spans will be transformed into pills when detected in message to send + */ +interface MatrixItemSpan { + val matrixItem: MatrixItem +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt new file mode 100644 index 0000000000000000000000000000000000000000..e84b75d0af8e4d8429eb58bf35814a42a01dcff6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.send + +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to send events in a room. It's implemented at the room level. + */ +interface SendService { + + /** + * Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead. + * @param eventType the type of the event + * @param content the optional body as a json dict. + * @return a [Cancelable] + */ + fun sendEvent(eventType: String, content: Content?): Cancelable + + /** + * Method to send a text message asynchronously. + * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated + * by the sdk into pills. + * @param text the text message to send + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @return a [Cancelable] + */ + fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + + /** + * Method to send a text message with a formatted body. + * @param text the text message to send + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @return a [Cancelable] + */ + fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + + /** + * Method to send a media asynchronously. + * @param attachment the media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @return a [Cancelable] + */ + fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set<String>): Cancelable + + /** + * Method to send a list of media asynchronously. + * @param attachments the list of media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @return a [Cancelable] + */ + fun sendMedias(attachments: List<ContentAttachmentData>, + compressBeforeSending: Boolean, + roomIds: Set<String>): Cancelable + + /** + * Send a poll to the room. + * @param question the question + * @param options list of (label, value) + * @return a [Cancelable] + */ + fun sendPoll(question: String, options: List<OptionItem>): Cancelable + + /** + * Method to send a poll response. + * @param pollEventId the poll currently replied to + * @param optionIndex The reply index + * @param optionValue The option value (for compatibility) + * @return a [Cancelable] + */ + fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable + + /** + * Redact (delete) the given event. + * @param event The event to redact + * @param reason Optional reason string + */ + fun redactEvent(event: Event, reason: String?): Cancelable + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendTextMessage(localEcho: TimelineEvent): Cancelable? + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? + + /** + * Remove this failed message from the timeline + * @param localEcho the unsent local echo + */ + fun deleteFailedEcho(localEcho: TimelineEvent) + + fun clearSendingQueue() + + /** + * Resend all failed messages one by one (and keep order) + */ + fun resendAllFailedMessages() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0dd2f3025c876996c511305c8f092dd1b95c2ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.send + +enum class SendState { + UNKNOWN, + // the event has not been sent + UNSENT, + // the event is encrypting + ENCRYPTING, + // the event is currently sending + SENDING, + // the event has been sent + SENT, + // the event has been received from server + SYNCED, + // The event failed to be sent + UNDELIVERED, + // the event failed to be sent because some unknown devices have been found while encrypting it + FAILED_UNKNOWN_DEVICES; + + internal companion object { + val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) + val IS_SENT_STATES = listOf(SENT, SYNCED) + val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) + val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES + } + + fun isSent() = IS_SENT_STATES.contains(this) + + fun hasFailed() = HAS_FAILED_STATES.contains(this) + + fun isSending() = IS_SENDING_STATES.contains(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5542b7d63bcab6b2b7ed40c92e82d51af3199a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.send + +/** + * Describes a user draft: + * REGULAR: draft of a classical message + * QUOTE: draft of a message which quotes another message + * EDIT: draft of an edition of a message + * REPLY: draft of a reply of another message + */ +sealed class UserDraft(open val text: String) { + data class REGULAR(override val text: String) : UserDraft(text) + data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) + data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) + data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) + + fun isValid(): Boolean { + return when (this) { + is REGULAR -> text.isNotBlank() + else -> true + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2836ecaaeb63033c702e94832576c06b05caa46 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.sender + +data class SenderInfo( + val userId: String, + /** + * Consider using [disambiguatedDisplayName] + */ + val displayName: String?, + val isUniqueDisplayName: Boolean, + val avatarUrl: String? +) { + val disambiguatedDisplayName: String + get() = when { + displayName.isNullOrBlank() -> userId + isUniqueDisplayName -> displayName + else -> "$displayName ($userId)" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt new file mode 100644 index 0000000000000000000000000000000000000000..f887a8b854d0437483cba839d2785a260fcbb3ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.state + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional + +interface StateService { + + /** + * Update the topic of the room + */ + fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the name of the room + */ + fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Add new alias to the room. + */ + fun addRoomAlias(roomAlias: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the canonical alias of the room + */ + fun updateCanonicalAlias(alias: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the history readability of the room + */ + fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the avatar of the room + */ + fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable + + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable + + fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? + + fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>> + + fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event> + + fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..62f9560315804eded8676ae16a2f77c08385ca7d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.tags + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to handle tags of a room. It's implemented at the room level. + */ +interface TagsService { + /** + * Add a tag to a room + */ + fun addTag(tag: String, order: Double?, callback: MatrixCallback<Unit>): Cancelable + + /** + * Remove tag from a room + */ + fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt new file mode 100644 index 0000000000000000000000000000000000000000..8920689d9721b9e2126af47e4c2b65ffbe33ef10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +/** + * A Timeline instance represents a contiguous sequence of events in a room. + * <p> + * There are two kinds of timeline: + * <p> + * - live timelines: they process live events from the sync. You can paginate + * backwards but not forwards. + * <p> + * - past timelines: they start in the past from an `initialEventId`. You can paginate + * backwards and forwards. + * + */ +interface Timeline { + + val timelineID: String + + val isLive: Boolean + + fun addListener(listener: Listener): Boolean + + fun removeListener(listener: Listener): Boolean + + fun removeAllListeners() + + /** + * This must be called before any other method after creating the timeline. It ensures the underlying database is open + */ + fun start() + + /** + * This must be called when you don't need the timeline. It ensures the underlying database get closed. + */ + fun dispose() + + /** + * This method restarts the timeline, erases all built events and pagination states. + * It then loads events around the eventId. If eventId is null, it does restart the live timeline. + */ + fun restartWithEventId(eventId: String?) + + /** + * Check if the timeline can be enriched by paginating. + * @param direction the direction to check in + * @return true if timeline can be enriched + */ + fun hasMoreToLoad(direction: Direction): Boolean + + /** + * This is the main method to enrich the timeline with new data. + * It will call the onTimelineUpdated method from [Listener] when the data will be processed. + * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. + */ + fun paginate(direction: Direction, count: Int) + + /** + * Returns the number of sending events + */ + fun pendingEventCount(): Int + + /** + * Returns the number of failed sending events. + */ + fun failedToDeliverEventCount(): Int + + /** + * Returns the index of a built event or null. + */ + fun getIndexOfEvent(eventId: String?): Int? + + /** + * Returns the built [TimelineEvent] at index or null + */ + fun getTimelineEventAtIndex(index: Int): TimelineEvent? + + /** + * Returns the built [TimelineEvent] with eventId or null + */ + fun getTimelineEventWithId(eventId: String?): TimelineEvent? + + /** + * Returns the first displayable events starting from eventId. + * It does depend on the provided [TimelineSettings]. + */ + fun getFirstDisplayableEventId(eventId: String): String? + + interface Listener { + /** + * Call when the timeline has been updated through pagination or sync. + * The latest event is the first in the list + * @param snapshot the most up to date snapshot + */ + fun onTimelineUpdated(snapshot: List<TimelineEvent>) + + /** + * Called whenever an error we can't recover from occurred + */ + fun onTimelineFailure(throwable: Throwable) + + /** + * Called when new events come through the sync + */ + fun onNewTimelineEvents(eventIds: List<String>) + } + + /** + * This is used to paginate in one or another direction. + */ + enum class Direction { + /** + * It represents future events. + */ + FORWARDS, + /** + * It represents past events. + */ + BACKWARDS + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f3c85afe69be1cb36ae5f25ffd6c85de37637cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.isReply +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply + +/** + * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. + * This class is used by [TimelineService] + * Users can also enrich it with metadata. + */ +data class TimelineEvent( + val root: Event, + val localId: Long, + val eventId: String, + val displayIndex: Int, + val senderInfo: SenderInfo, + val annotations: EventAnnotationsSummary? = null, + val readReceipts: List<ReadReceipt> = emptyList() +) { + + init { + if (BuildConfig.DEBUG) { + assert(eventId == root.eventId) + } + } + + val metadata = HashMap<String, Any>() + + /** + * The method to enrich this timeline event. + * If you provides multiple data with the same key, only first one will be kept. + * @param key the key to associate data with. + * @param data the data to enrich with. + */ + fun enrichWith(key: String?, data: Any?) { + if (key == null || data == null) { + return + } + if (!metadata.containsKey(key)) { + metadata[key] = data + } + } + + /** + * Get the metadata associated with a key. + * @param key the key to get the metadata + * @return the metadata + */ + inline fun <reified T> getMetadata(key: String): T? { + return metadata[key] as T? + } + + fun isEncrypted(): Boolean { + // warning: Do not use getClearType here + return EventType.ENCRYPTED == root.type + } +} + +/** + * Tells if the event has been edited + */ +fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null + +/** + * Get the relation content if any + */ +fun TimelineEvent.getRelationContent(): RelationDefaultContent? { + return root.getRelationContent() +} + +/** + * Get the eventId which was edited by this event if any + */ +fun TimelineEvent.getEditedEventId(): String? { + return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId +} + +/** + * Get last MessageContent, after a possible edition + */ +fun TimelineEvent.getLastMessageContent(): MessageContent? { + return if (root.getClearType() == EventType.STICKER) { + root.getClearContent().toModel<MessageStickerContent>() + } else { + annotations?.editSummary?.aggregatedContent?.toModel() + ?: root.getClearContent().toModel() + } +} + +/** + * Get last Message body, after a possible edition + */ +fun TimelineEvent.getLastMessageBody(): String? { + val lastMessageContent = getLastMessageContent() + + if (lastMessageContent != null) { + return lastMessageContent.newContent?.toModel<MessageContent>()?.body + ?: lastMessageContent.body + } + + return null +} + +/** + * Returns true if it's a reply + */ +fun TimelineEvent.isReply(): Boolean { + return root.isReply() +} + +fun TimelineEvent.getTextEditableContent(): String? { + val lastContent = getLastMessageContent() + return if (isReply()) { + return extractUsefulTextFromReply(lastContent?.body ?: "") + } else { + lastContent?.body ?: "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt new file mode 100644 index 0000000000000000000000000000000000000000..473e5053020ef2e1fab62687a58dba9bf2e111c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to interact with the timeline. It's implemented at the room level. + */ +interface TimelineService { + + /** + * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. + * You can also configure some settings with the [settings] param. + * + * Important: the returned Timeline has to be started + * + * @param eventId the optional initial eventId. + * @param settings settings to configure the timeline. + * @return the instantiated timeline + */ + fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline + + fun getTimeLineEvent(eventId: String): TimelineEvent? + + fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> + + fun getAttachmentMessages() : List<TimelineEvent> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..4f915cb907037b11e82ff3da9926dba6715cf414 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +/** + * Data class holding setting values for a [Timeline] instance. + */ +data class TimelineSettings( + /** + * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. + */ + val initialSize: Int, + /** + * A flag to filter edit events + */ + val filterEdits: Boolean = false, + /** + * A flag to filter redacted events + */ + val filterRedacted: Boolean = false, + /** + * A flag to filter useless events, such as membership events without any change + */ + val filterUseless: Boolean = false, + /** + * A flag to filter by types. It should be used with [allowedTypes] field + */ + val filterTypes: Boolean = false, + /** + * If [filterTypes] is true, the list of types allowed by the list. + */ + val allowedTypes: List<String> = emptyList(), + /** + * If true, will build read receipts for each event. + */ + val buildReadReceipts: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..eaa8d5c3df3951d9e6c567092d8bd7d105ae699c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.typing + +/** + * This interface defines methods to handle typing data. It's implemented at the room level. + */ +interface TypingService { + + /** + * To call when user is typing a message in the room + * The SDK will handle the requests scheduling to the homeserver: + * - No more than one typing request per 10s + * - If not called after 10s, the SDK will notify the homeserver that the user is not typing anymore + */ + fun userIsTyping() + + /** + * To call when user stops typing in the room + * Notify immediately the homeserver that the user is not typing anymore in the room, for + * instance when user has emptied the composer, or when the user quits the timeline screen. + */ + fun userStopsTyping() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..09b885e24dee54f6c270145422fae368a2367c49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +data class GetUploadsResult( + // List of fetched Events, most recent first + val uploadEvents: List<UploadEvent>, + // token to get more events + val nextToken: String, + // True if there are more event to load + val hasMore: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..16423cf3c5ee3ec52a65c259419862188c1b4c08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +/** + * Wrapper around on Event. + * Similar to [org.matrix.android.sdk.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data + */ +data class UploadEvent( + val root: Event, + val eventId: String, + val contentWithAttachmentContent: MessageWithAttachmentContent, + val senderInfo: SenderInfo +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..1cabdfc92afc99fd69d0060ee772208e2d76a9d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. + */ +interface UploadsService { + + /** + * Get a list of events containing URL sent to a room, from most recent to oldest one + * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. + * @param since token to get next page, or null to get the first page + */ + fun getUploads(numberOfEvents: Int, + since: String?, + callback: MatrixCallback<GetUploadsResult>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c062b42037d61da515295c6cbd877e2d6f3290e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataContent + +/** + * The account_data will have an encrypted property that is a map from key ID to an object. + * The algorithm from the m.secret_storage.key.[key ID] data for the given key defines how the other properties are interpreted, + * though it's expected that most encryption schemes would have ciphertext and mac properties, + * where the ciphertext property is the unpadded base64-encoded ciphertext, and the mac is used to ensure the integrity of the data. + */ +@JsonClass(generateAdapter = true) +data class EncryptedSecretContent( + /** unpadded base64-encoded ciphertext */ + @Json(name = "ciphertext") val ciphertext: String? = null, + @Json(name = "mac") val mac: String? = null, + @Json(name = "ephemeral") val ephemeral: String? = null, + @Json(name = "iv") val initializationVector: String? = null +) : AccountDataContent { + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): EncryptedSecretContent? { + return MoshiProvider.providesMoshi() + .adapter(EncryptedSecretContent::class.java) + .fromJsonValue(obj) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..096f9f34a2c71c3f97240fdc5dda2112e0c4afeb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class IntegrityResult { + data class Success(val passphraseBased: Boolean) : IntegrityResult() + data class Error(val cause: SharedSecretStorageError) : IntegrityResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..287555ae95be4ac0379db23c47a7ce78315372b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class KeyInfoResult { + data class Success(val keyInfo: KeyInfo) : KeyInfoResult() + data class Error(val error: SharedSecretStorageError) : KeyInfoResult() + + fun isSuccess(): Boolean = this is Success +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d56fb81f3815d76af11adb207ca8879573d23a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +interface KeySigner { + fun sign(canonicalJson: String): Map<String, Map<String, String>>? +} + +class EmptyKeySigner : KeySigner { + override fun sign(canonicalJson: String): Map<String, Map<String, String>>? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..f960a4367522b5e0396b17f58769fdb453231219 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +/** + * + * The contents of the account data for the key will include an algorithm property, which indicates the encryption algorithm used, as well as a name property, + * which is a human-readable name. + * The contents will be signed as signed JSON using the user's master cross-signing key. Other properties depend on the encryption algorithm. + * + * + * "content": { + * "algorithm": "m.secret_storage.v1.curve25519-aes-sha2", + * "passphrase": { + * "algorithm": "m.pbkdf2", + * "iterations": 500000, + * "salt": "IrswcMWnYieBALCAOMBw9k93xSzlc2su" + * }, + * "pubkey": "qql1q3IvBbwMU97zLnyh9HYW5x/zqTy5eoK1n+9fm1Y", + * "signatures": { + * "@valere35:matrix.org": { + * "ed25519:nOUQYiH9L8uKp5JajqiQyv+Loa3+lsdil7UBverz/Ko": "QtePmwfUL7+SHYRJT/HaTgF7gUFog1E/wtUCt0qc5aB8N+Sz5iCOvQ0KtaFHQ5SJzsBlYH8k7ejoBc0RcnU7BA" + * } + * } + * } + */ + +data class KeyInfo( + val id: String, + val content: SecretStorageKeyContent +) + +@JsonClass(generateAdapter = true) +data class SecretStorageKeyContent( + /** Currently support m.secret_storage.v1.curve25519-aes-sha2 */ + @Json(name = "algorithm") val algorithm: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "passphrase") val passphrase: SsssPassphrase? = null, + @Json(name = "pubkey") val publicKey: String? = null, + @Json(name = "signatures") val signatures: Map<String, Map<String, String>>? = null +) { + + private fun signalableJSONDictionary(): Map<String, Any> { + return mutableMapOf<String, Any>().apply { + algorithm + ?.let { this["algorithm"] = it } + name + ?.let { this["name"] = it } + publicKey + ?.let { this["pubkey"] = it } + passphrase + ?.let { ssssPassphrase -> + this["passphrase"] = mapOf( + "algorithm" to ssssPassphrase.algorithm, + "iterations" to ssssPassphrase.iterations, + "salt" to ssssPassphrase.salt + ) + } + } + } + + fun canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) + } + + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): SecretStorageKeyContent? { + return MoshiProvider.providesMoshi() + .adapter(SecretStorageKeyContent::class.java) + .fromJsonValue(obj) + } + } +} + +@JsonClass(generateAdapter = true) +data class SsssPassphrase( + @Json(name = "algorithm") val algorithm: String?, + @Json(name = "iterations") val iterations: Int, + @Json(name = "salt") val salt: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt new file mode 100644 index 0000000000000000000000000000000000000000..89095268b3bc0e38382d3fb05bdba1dc0bca1e01 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import java.io.InputStream +import java.io.OutputStream + +interface SecureStorageService { + + fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) + + fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt new file mode 100644 index 0000000000000000000000000000000000000000..79e7fa51fe41e1e469a1bf738b164f737f52c830 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class SharedSecretStorageError(message: String?) : Throwable(message) { + data class UnknownSecret(val secretName: String) : SharedSecretStorageError("Unknown Secret $secretName") + data class UnknownKey(val keyId: String) : SharedSecretStorageError("Unknown key $keyId") + data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId") + data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm") + data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName") + data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) + : SharedSecretStorageError("Missing content for secret $secretName with key $keyId") + + object BadKeyFormat : SharedSecretStorageError("Bad Key Format") + object ParsingError : SharedSecretStorageError("parsing Error") + object BadMac : SharedSecretStorageError("Bad mac") + object BadCipherText : SharedSecretStorageError("Bad cipher text") + + data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ffc7e3889dd752fad44aa7ec115a48c1f2d8e0c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +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 +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME + +/** + * Some features may require clients to store encrypted data on the server so that it can be shared securely between clients. + * Clients may also wish to securely send such data directly to each other. + * For example, key backups (MSC1219) can store the decryption key for the backups on the server, or cross-signing (MSC1756) can store the signing keys. + * + * https://github.com/matrix-org/matrix-doc/pull/1946 + * + */ + +interface SharedSecretStorageService { + + /** + * Generates a SSSS key for encrypting secrets. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) + * + * @param keyId the ID of the key + * @param key keep null if you want to generate a random key + * @param keyName a human readable name + * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) + * + * @param callback Get key creation info + */ + fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback<SsssKeyCreationInfo>) + + /** + * Generates a SSSS key using the given passphrase. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key, salt, iteration ...) + * + * @param keyId the ID of the key + * @param keyName human readable key name + * @param passphrase The passphrase used to generate the key + * @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret) + * @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress + * + * @param callback Get key creation info + */ + fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback<SsssKeyCreationInfo>) + + fun getKey(keyId: String): KeyInfoResult + + /** + * A key can be marked as the "default" key by setting the user's account_data with event type m.secret_storage.default_key + * to an object that has the ID of the key as its key property. + * The default key will be used to encrypt all secrets that the user would expect to be available on all their clients. + * Unless the user specifies otherwise, clients will try to use the default key to decrypt secrets. + */ + fun getDefaultKey(): KeyInfoResult + + fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) + + /** + * Check whether we have a key with a given ID. + * + * @param keyId The ID of the key to check + * @return Whether we have the key. + */ + fun hasKey(keyId: String): Boolean + + /** + * Store an encrypted secret on the server + * Clients MUST ensure that the key is trusted before using it to encrypt secrets. + * + * @param name The name of the secret + * @param secret The secret contents. + * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. + */ + fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>, callback: MatrixCallback<Unit>) + + /** + * Use this call to determine which SSSSKeySpec to use for requesting secret + */ + fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> + + /** + * Get an encrypted secret from the shared storage + * + * @param name The name of the secret + * @param keyId The id of the key that should be used to decrypt (null for default key) + * @param secretKey the secret key to use (@see #RawBytesKeySpec) + * + */ + fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>) + + /** + * Return true if SSSS is configured + */ + fun isRecoverySetup(): Boolean { + return checkShouldBeAbleToAccessSecrets( + secretNames = listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME), + keyId = null + ) is IntegrityResult.Success + } + + fun isMegolmKeyInBackup(): Boolean { + return checkShouldBeAbleToAccessSecrets( + secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME), + keyId = null + ) is IntegrityResult.Success + } + + fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult + + fun requestSecret(name: String, myOtherDeviceId: String) + + data class KeyRef( + val keyId: String?, + val keySpec: SsssKeySpec? + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..17f0366d167b48c6c89eeec79a1a7712e9ab4621 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +data class SsssKeyCreationInfo( + val keyId: String = "", + var content: SecretStorageKeyContent?, + val recoveryKey: String = "", + val keySpec: SsssKeySpec +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ae181a44e95a5d5ffdbbb8cf90a87592abc0ca3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.internal.crypto.keysbackup.deriveKey +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey + +/** Tag class */ +interface SsssKeySpec + +data class RawBytesKeySpec( + val privateKey: ByteArray +) : SsssKeySpec { + + companion object { + + fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec { + return RawBytesKeySpec( + privateKey = deriveKey( + passphrase, + salt, + iterations, + progressListener + ) + ) + } + + fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? { + return extractCurveKeyFromRecoveryKey(recoveryKey)?.let { + RawBytesKeySpec( + privateKey = it + ) + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawBytesKeySpec + + if (!privateKey.contentEquals(other.privateKey)) return false + + return true + } + + override fun hashCode(): Int { + return privateKey.contentHashCode() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4061c5c7c7f484b096b148cb0a2d443e6b5f4ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.signout + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines a method to sign out, or to renew the token. It's implemented at the session level. + */ +interface SignOutService { + + /** + * Ask the homeserver for a new access token. + * The same deviceId will be used + */ + fun signInAgain(password: String, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the session with credentials received after SSO + */ + fun updateCredentials(credentials: Credentials, + callback: MatrixCallback<Unit>): Cancelable + + /** + * Sign out, and release the session, clear all the session data, including crypto data + * @param signOutFromHomeserver true if the sign out request has to be done + */ + fun signOut(signOutFromHomeserver: Boolean, + callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..03899699483d0d24809519956649e1192bd51a2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.sync + +interface FilterService { + + enum class FilterPreset { + NoFilter, + /** + * Filter for Riot, will include only known event type + */ + RiotFilter + } + + /** + * Configure the filter for the sync + */ + fun setFilter(filterPreset: FilterPreset) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt new file mode 100644 index 0000000000000000000000000000000000000000..08d8be699a64efdc7db8e2dfefd8890bc4087b9b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.sync + +sealed class SyncState { + object Idle : SyncState() + data class Running(val afterPause: Boolean) : SyncState() + object Paused : SyncState() + object Killing : SyncState() + object Killed : SyncState() + object NoNetwork : SyncState() + object InvalidToken : SyncState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..685f4ba9c3c3e06e7d89b12d189ed60942676b68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.terms + +import org.matrix.android.sdk.internal.session.terms.TermsResponse + +data class GetTermsResponse( + val serverResponse: TermsResponse, + val alreadyAcceptedTermUrls: Set<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e2201cb2991c49d086cdcfd310af683f97b3828 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.terms + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface TermsService { + enum class ServiceType { + IntegrationManager, + IdentityService + } + + fun getTerms(serviceType: ServiceType, + baseUrl: String, + callback: MatrixCallback<GetTermsResponse>): Cancelable + + fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List<String>, + token: String?, + callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..e51fa45d72891f9c2d8cea49779fa52c78d0aadd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.typing + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +/** + * Responsible for tracking typing users from each room. + * It's ephemeral data and it's only saved in memory. + */ +interface TypingUsersTracker { + + /** + * Returns the sender information of all currently typing users in a room, excluding yourself. + */ + fun getTypingUsers(roomId: String): List<SenderInfo> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5617a206fa756b2e19946d6146bf3368276cabf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.user + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to get users. It's implemented at the session level. + */ +interface UserService { + + /** + * Get a user from a userId + * @param userId the userId to look for. + * @return a user with userId or null + */ + fun getUser(userId: String): User? + + /** + * Search list of users on server directory. + * @param search the searched term + * @param limit the max number of users to return + * @param excludedUserIds the user ids to filter from the search + * @param callback the async callback + * @return Cancelable + */ + fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable + + /** + * Observe a live user from a userId + * @param userId the userId to look for. + * @return a LiveData of user with userId + */ + fun getUserLive(userId: String): LiveData<Optional<User>> + + /** + * Observe a live list of users sorted alphabetically + * @return a Livedata of users + */ + fun getUsersLive(): LiveData<List<User>> + + /** + * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. + * @param filter the filter. It will look into userId and displayName. + * @param excludedUserIds userId list which will be excluded from the result list. + * @return a Livedata of users + */ + fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set<String>? = null): LiveData<PagedList<User>> + + /** + * Get list of ignored users + */ + fun getIgnoredUsersLive(): LiveData<List<User>> + + /** + * Ignore users + */ + fun ignoreUserIds(userIds: List<String>, callback: MatrixCallback<Unit>): Cancelable + + /** + * Un-ignore some users + */ + fun unIgnoreUserIds(userIds: List<String>, callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf8551588ed8ee8b58e0b3936ec25d40089c3a80 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.user.model + +/** + * Data class which holds information about a user. + * It can be retrieved with [org.matrix.android.sdk.api.session.user.UserService] + */ +data class User( + val userId: String, + /** + * For usage in UI, consider using [getBestName] + */ + val displayName: String? = null, + val avatarUrl: String? = null +) { + /** + * Return the display name or the user id + */ + fun getBestName() = displayName?.takeIf { it.isNotEmpty() } ?: userId +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt new file mode 100644 index 0000000000000000000000000000000000000000..abbbf040ab81a0e389ca481916aa52415a85ff52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets + +import org.matrix.android.sdk.api.failure.Failure + +sealed class WidgetManagementFailure : Failure.FeatureFailure() { + object NotEnoughPower : WidgetManagementFailure() + object CreationFailed : WidgetManagementFailure() + data class TermsNotSignedException(val baseUrl: String, val token: String) : WidgetManagementFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..4dba2a10e1d018a974ab88b199a1776f05fbcf3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets + +import android.webkit.WebView +import org.matrix.android.sdk.api.util.JsonDict +import java.lang.reflect.Type + +interface WidgetPostAPIMediator { + + /** + * This initialize the webview to handle. + * It will add a JavaScript Interface. + * Please call [clearWebView] method when finished to clean the provided webview + */ + fun setWebView(webView: WebView) + + /** + * Set handler to communicate with the widgetPostAPIMediator. + * Please remove the reference by passing null when finished. + */ + fun setHandler(handler: Handler?) + + /** + * This clear the mediator by removing the JavaScript Interface and cleaning references. + */ + fun clearWebView() + + /** + * Inject the necessary javascript into the configured WebView. + * Should be called after a web page has been loaded. + */ + fun injectAPI() + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + fun sendBoolResponse(response: Boolean, eventData: JsonDict) + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + fun sendIntegerResponse(response: Int, eventData: JsonDict) + + /** + * Send an object response + * + * @param klass the class of the response + * @param response the response + * @param eventData the modular data + */ + fun <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict) + + /** + * Send success + * + * @param eventData the modular data + */ + fun sendSuccess(eventData: JsonDict) + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + fun sendError(message: String, eventData: JsonDict) + + interface Handler { + /** + * Triggered when a widget is posting + */ + fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt new file mode 100644 index 0000000000000000000000000000000000000000..444708d992add767ef3b6ce5198bcad7639aa446 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.session.widgets.model.Widget + +/** + * This is the entry point to manage widgets. You can grab an instance of this service through an active session. + */ +interface WidgetService { + + /** + * Returns an instance of [WidgetURLFormatter]. + */ + fun getWidgetURLFormatter(): WidgetURLFormatter + + /** + * Returns a new instance of [WidgetPostAPIMediator]. + * Be careful to call clearWebView method and setHandler to null to avoid memory leaks. + * This is to be used for "admin" widgets so you can interact through JS. + */ + fun getWidgetPostAPIMediator(): WidgetPostAPIMediator + + /** + * Returns the current room widgets defined through state events. + * Some widgets can be deactivated, so be sure to check for isActive if needed. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): List<Widget> + + /** + * Returns the live room widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): LiveData<List<Widget>> + + /** + * Returns the current user widgets. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgets( + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): List<Widget> + + /** + * Returns the live user widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgetsLive( + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): LiveData<List<Widget>> + + /** + * Creates a new widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable + + /** + * Deactivate a widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Returns true if you can add/remove widgets. It goes through + * @param roomId the room where you want to administrate widgets. + */ + fun hasPermissionsToHandleWidgets(roomId: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt new file mode 100644 index 0000000000000000000000000000000000000000..ad01679ee507a50a03c7aa4cceed3da758a15b43 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets + +interface WidgetURLFormatter { + /** + * Takes care of fetching a scalar token if required and build the final url. + * This methods can throw, you should take care of handling failure. + * + * @param baseUrl the baseUrl which will be checked for scalar token + * @param params additional params you want to append to the base url. + * @param forceFetchScalarToken if true, you will force to fetch a new scalar token + * from the server (only if the base url is whitelisted) + * @param bypassWhitelist if true, the base url will be considered as whitelisted + */ + suspend fun format( + baseUrl: String, + params: Map<String, String> = emptyMap(), + forceFetchScalarToken: Boolean = false, + bypassWhitelist: Boolean + ): String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt new file mode 100644 index 0000000000000000000000000000000000000000..9da2f224f7d11e8d10b559bbf1a674758219dd66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +data class Widget( + val widgetContent: WidgetContent, + val event: Event, + val widgetId: String, + val senderInfo: SenderInfo?, + val isAddedByMe: Boolean, + val computedUrl: String?, + val type: WidgetType +) { + + val isActive = widgetContent.isActive() + + val name = widgetContent.getHumanName() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a3d39737665364dc819f835b142e1cc86aa0c06 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +import android.annotation.SuppressLint +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 + */ +@JsonClass(generateAdapter = true) +data class WidgetContent( + @Json(name = "creatorUserId") val creatorUserId: String? = null, + @Json(name = "id") val id: String? = null, + @Json(name = "type") val type: String? = null, + @Json(name = "url") val url: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "data") val data: JsonDict = emptyMap(), + @Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false +) { + + fun isActive() = type != null && url != null + + @SuppressLint("DefaultLocale") + fun getHumanName(): String { + return (name ?: type ?: "").capitalize() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt new file mode 100644 index 0000000000000000000000000000000000000000..278a123699d64d633e32e9fe7a47dac85d486d4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +private val DEFINED_TYPES by lazy { + listOf( + WidgetType.Jitsi, + WidgetType.TradingView, + WidgetType.Spotify, + WidgetType.Video, + WidgetType.GoogleDoc, + WidgetType.GoogleCalendar, + WidgetType.Etherpad, + WidgetType.StickerPicker, + WidgetType.Grafana, + WidgetType.Custom, + WidgetType.IntegrationManager + ) +} + +/** + * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 + */ +sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) { + object Jitsi : WidgetType("m.jitsi", "jitsi") + object TradingView : WidgetType("m.tradingview") + object Spotify : WidgetType("m.spotify") + object Video : WidgetType("m.video") + object GoogleDoc : WidgetType("m.googledoc") + object GoogleCalendar : WidgetType("m.googlecalendar") + object Etherpad : WidgetType("m.etherpad") + object StickerPicker : WidgetType("m.stickerpicker") + object Grafana : WidgetType("m.grafana") + object Custom : WidgetType("m.custom") + object IntegrationManager : WidgetType("m.integration_manager") + data class Fallback(override val preferred: String) : WidgetType(preferred) + + fun matches(type: String): Boolean { + return type == preferred || type == legacy + } + + fun values(): Set<String> { + return setOf(preferred, legacy) + } + + companion object { + + fun fromString(type: String): WidgetType { + val matchingType = DEFINED_TYPES.firstOrNull { + it.matches(type) + } + return matchingType ?: Fallback(type) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1976f3921db51ccfae2dc1729bae121ba79aebf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +/** + * An interface defining a unique cancel method. + * It should be used with methods you want to be able to cancel, such as ones interacting with Web Services. + */ +interface Cancelable { + + /** + * The cancel method, it does nothing by default. + */ + fun cancel() { + // no-op + } +} + +object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc44e08c02eae22cce05b67b5bef7e3268cc4e3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() { + override fun cancel() { + forEach { it.cancel() } + clear() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..a11be9629797d572c33069b475b16ca220cac90e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.util + +object ContentUtils { + fun extractUsefulTextFromReply(repliedBody: String): String { + val lines = repliedBody.lines() + var wellFormed = repliedBody.startsWith(">") + var endOfPreviousFound = false + val usefullines = ArrayList<String>() + lines.forEach { + if (it == "") { + endOfPreviousFound = true + return@forEach + } + if (!endOfPreviousFound) { + wellFormed = wellFormed && it.startsWith(">") + } else { + usefullines.add(it) + } + } + return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody + } + + fun extractUsefulTextFromHtmlReply(repliedBody: String): String { + if (repliedBody.startsWith("<mx-reply>")) { + val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>") + if (closingTagIndex != -1) { + return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim() + } + } + return repliedBody + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt new file mode 100644 index 0000000000000000000000000000000000000000..c72ae3d0518702c733f4cd70c050e39b38821a7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +import org.matrix.android.sdk.api.MatrixCallback + +/** + * Simple MatrixCallback implementation which delegate its calls to another callback + */ +open class MatrixCallbackDelegate<T>(private val callback: MatrixCallback<T>) : MatrixCallback<T> by callback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e99ae52b41021247eaf23871591f666752bf8be --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.user.model.User +import java.util.Locale + +sealed class MatrixItem( + open val id: String, + open val displayName: String?, + open val avatarUrl: String? +) { + data class UserItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class EventItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class RoomItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class RoomAliasItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id + } + + data class GroupItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id + } + + open fun getBestName(): String { + return displayName?.takeIf { it.isNotBlank() } ?: id + } + + protected fun checkId() { + if (!id.startsWith(getIdPrefix())) { + error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}") + } + } + + /** + * Return the prefix as defined in the matrix spec (and not extracted from the id) + */ + fun getIdPrefix() = when (this) { + is UserItem -> '@' + is EventItem -> '$' + is RoomItem -> '!' + is RoomAliasItem -> '#' + is GroupItem -> '+' + } + + fun firstLetterOfDisplayName(): String { + return (displayName?.takeIf { it.isNotBlank() } ?: id) + .let { dn -> + var startIndex = 0 + val initial = dn[startIndex] + + if (initial in listOf('@', '#', '+') && dn.length > 1) { + startIndex++ + } + + var length = 1 + var first = dn[startIndex] + + // LEFT-TO-RIGHT MARK + if (dn.length >= 2 && 0x200e == first.toInt()) { + startIndex++ + first = dn[startIndex] + } + + // check if it’s the start of a surrogate pair + if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) { + val second = dn[startIndex + 1] + if (second.toInt() in 0xDC00..0xDFFF) { + length++ + } + } + + dn.substring(startIndex, startIndex + length) + } + .toUpperCase(Locale.ROOT) + } + + companion object { + private const val ircPattern = " (IRC)" + } +} + +/* ========================================================================================== + * Extensions to create MatrixItem + * ========================================================================================== */ + +fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) + +fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) + +fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) + +// If no name is available, use room alias as Riot-Web does +fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) + +fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt new file mode 100644 index 0000000000000000000000000000000000000000..159f7149b92a7da3408516534952f83c0373d116 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt @@ -0,0 +1,58 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package org.matrix.android.sdk.api.util + +data class Optional<T : Any> constructor(private val value: T?) { + + fun get(): T { + return value!! + } + + fun getOrNull(): T? { + return value + } + + fun <U : Any> map(fn: (T) -> U?): Optional<U> { + return if (value == null) { + from(null) + } else { + from(fn(value)) + } + } + + fun getOrElse(fn: () -> T): T { + return value ?: fn() + } + + fun hasValue(): Boolean { + return value != null + } + + companion object { + fun <T : Any> from(value: T?): Optional<T> { + return Optional(value) + } + + fun <T: Any> empty(): Optional<T> { + return Optional(null) + } + } +} + +fun <T : Any> T?.toOptional() = Optional(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt new file mode 100644 index 0000000000000000000000000000000000000000..7344dab8d43d6301b800264c8759bb56f8dc0361 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +import com.squareup.moshi.Types +import java.lang.reflect.ParameterizedType + +typealias JsonDict = Map<String, @JvmSuppressWildcards Any> + +val emptyJsonDict = emptyMap<String, Any>() + +internal val JSON_DICT_PARAMETERIZED_TYPE: ParameterizedType = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..24f5558b263ec79896d83d2218b27ca551d9bf1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal + +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.session.DaggerSessionComponent +import org.matrix.android.sdk.internal.session.SessionComponent +import javax.inject.Inject + +@MatrixScope +internal class SessionManager @Inject constructor(private val matrixComponent: MatrixComponent, + private val sessionParamsStore: SessionParamsStore) { + + // SessionId -> SessionComponent + private val sessionComponents = HashMap<String, SessionComponent>() + + fun getSessionComponent(sessionId: String): SessionComponent? { + val sessionParams = sessionParamsStore.get(sessionId) ?: return null + return getOrCreateSessionComponent(sessionParams) + } + + fun getOrCreateSession(sessionParams: SessionParams): Session { + return getOrCreateSessionComponent(sessionParams).session() + } + + fun releaseSession(sessionId: String) { + if (sessionComponents.containsKey(sessionId).not()) { + throw RuntimeException("You don't have a session for id $sessionId") + } + sessionComponents.remove(sessionId)?.also { + it.session().close() + } + } + + private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { + return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { + DaggerSessionComponent + .factory() + .create(matrixComponent, sessionParams) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..00eb7e859969026dfe8606a7cec8b9bd5a56e553 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.auth.data.RiotConfig +import org.matrix.android.sdk.internal.auth.data.TokenLoginParams +import org.matrix.android.sdk.internal.auth.login.ResetPasswordMailConfirmed +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse +import org.matrix.android.sdk.internal.auth.registration.RegistrationParams +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Url + +/** + * The login REST API. + */ +internal interface AuthAPI { + + /** + * Get a Riot config file + */ + @GET("config.json") + fun getRiotConfig(): Call<RiotConfig> + + /** + * Get the version information of the homeserver + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun versions(): Call<Versions> + + /** + * Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete + * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(@Body registrationParams: RegistrationParams): Call<Credentials> + + /** + * Add 3Pid during registration + * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928 + * https://github.com/matrix-org/matrix-doc/pull/2290 + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken") + fun add3Pid(@Path("threePid") threePid: String, + @Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse> + + /** + * Validate 3pid + */ + @POST + fun validate3Pid(@Url url: String, + @Body params: ValidationCodeBody): Call<SuccessResult> + + /** + * Get the supported login flow + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun getLoginFlows(): Call<LoginFlowResponse> + + /** + * Pass params to the server for the current login phase. + * Set all the timeouts to 1 minute + * + * @param loginParams the login parameters + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: PasswordLoginParams): Call<Credentials> + + // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: TokenLoginParams): Call<Credentials> + + /** + * Ask the homeserver to reset the password associated with the provided email. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken") + fun resetPassword(@Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse> + + /** + * Ask the homeserver to reset the password with the provided new password once the email is validated. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") + fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..229baac052c220f7082fefe63329a0d04de31fec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.auth.db.AuthRealmMigration +import org.matrix.android.sdk.internal.auth.db.AuthRealmModule +import org.matrix.android.sdk.internal.auth.db.RealmPendingSessionStore +import org.matrix.android.sdk.internal.auth.db.RealmSessionParamsStore +import org.matrix.android.sdk.internal.auth.login.DefaultDirectLoginTask +import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.AuthDatabase +import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter +import org.matrix.android.sdk.internal.wellknown.WellknownModule +import io.realm.RealmConfiguration +import java.io.File + +@Module(includes = [WellknownModule::class]) +internal abstract class AuthModule { + + @Module + companion object { + private const val DB_ALIAS = "matrix-sdk-auth" + + @JvmStatic + @Provides + @AuthDatabase + fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration { + val old = File(context.filesDir, "matrix-sdk-auth") + if (old.exists()) { + old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) + } + + return RealmConfiguration.Builder() + .apply { + realmKeysUtils.configureEncryption(this, DB_ALIAS) + } + .name("matrix-sdk-auth.realm") + .modules(AuthRealmModule()) + .schemaVersion(AuthRealmMigration.SCHEMA_VERSION) + .migration(AuthRealmMigration) + .build() + } + } + + @Binds + abstract fun bindLegacySessionImporter(importer: DefaultLegacySessionImporter): LegacySessionImporter + + @Binds + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore + + @Binds + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore + + @Binds + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService + + @Binds + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @Binds + abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..1294855b6edef86a7302fa3438a0758b49e90c2f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -0,0 +1,369 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import android.net.Uri +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.Credentials +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.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse +import org.matrix.android.sdk.internal.auth.data.RiotConfig +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard +import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk +import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.exhaustive +import org.matrix.android.sdk.internal.util.toCancelable +import org.matrix.android.sdk.internal.wellknown.GetWellknownTask +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal class DefaultAuthenticationService @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor +) : AuthenticationService { + + private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() + + private var currentLoginWizard: LoginWizard? = null + private var currentRegistrationWizard: RegistrationWizard? = null + + override fun hasAuthenticatedSessions(): Boolean { + return sessionParamsStore.getLast() != null + } + + override fun getLastAuthenticatedSession(): Session? { + val sessionParams = sessionParamsStore.getLast() + return sessionParams?.let { + sessionManager.getOrCreateSession(it) + } + } + + override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable { + val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig + + return if (homeServerConnectionConfig == null) { + callback.onFailure(IllegalStateException("Session not found")) + NoOpCancellable + } else { + getLoginFlow(homeServerConnectionConfig, callback) + } + } + + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable { + pendingSessionData = null + + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { + pendingSessionStore.delete() + + val result = runCatching { + getLoginFlowInternal(homeServerConnectionConfig) + } + result.fold( + { + if (it is LoginFlowResult.Success) { + // The homeserver exists and up to date, keep the config + // Homeserver url may have been changed, if it was a Riot url + val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(it.homeServerUrl) + ) + + pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) + .also { data -> pendingSessionStore.savePendingSessionData(data) } + } + callback.onSuccess(it) + }, + { + if (it is UnrecognizedCertificateException) { + callback.onFailure(Failure.UnrecognizedCertificateFailure(homeServerConnectionConfig.homeServerUri.toString(), it.fingerprint)) + } else { + callback.onFailure(it) + } + } + ) + } + .toCancelable() + } + + private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + return withContext(coroutineDispatchers.io) { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // First check the homeserver version + runCatching { + executeRequest<Versions>(null) { + apiCall = authAPI.versions() + } + } + .map { versions -> + // Ok, it seems that the homeserver url is valid + getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString()) + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // It's maybe a Riot url? + getRiotLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + } + + private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // Ok, try to get the config.json file of a RiotWeb client + return runCatching { + executeRequest<RiotConfig>(null) { + apiCall = authAPI.getRiotConfig() + } + } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest<Versions>(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId, homeServerConnectionConfig)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest<Versions>(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive + } + + private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { + return if (versions.isSupportedBySdk()) { + // Get the login flow + val loginFlowResponse = executeRequest<LoginFlowResponse>(null) { + apiCall = authAPI.getLoginFlows() + } + LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) + } else { + // Not supported + LoginFlowResult.OutdatedHomeserver + } + } + + override fun getRegistrationWizard(): RegistrationWizard { + return currentRegistrationWizard + ?: let { + pendingSessionData?.homeServerConnectionConfig?.let { + DefaultRegistrationWizard( + buildClient(it), + retrofitFactory, + coroutineDispatchers, + sessionCreator, + pendingSessionStore, + taskExecutor.executorScope + ).also { + currentRegistrationWizard = it + } + } ?: error("Please call getLoginFlow() with success first") + } + } + + override val isRegistrationStarted: Boolean + get() = currentRegistrationWizard?.isRegistrationStarted == true + + override fun getLoginWizard(): LoginWizard { + return currentLoginWizard + ?: let { + pendingSessionData?.homeServerConnectionConfig?.let { + DefaultLoginWizard( + buildClient(it), + retrofitFactory, + coroutineDispatchers, + sessionCreator, + pendingSessionStore, + taskExecutor.executorScope + ).also { + currentLoginWizard = it + } + } ?: error("Please call getLoginFlow() with success first") + } + } + + override fun cancelPendingLoginOrRegistration() { + currentLoginWizard = null + currentRegistrationWizard = null + + // Keep only the home sever config + // Update the local pendingSessionData synchronously + pendingSessionData = pendingSessionData?.homeServerConnectionConfig + ?.let { PendingSessionData(it) } + .also { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + if (it == null) { + // Should not happen + pendingSessionStore.delete() + } else { + pendingSessionStore.savePendingSessionData(it) + } + } + } + } + + override fun reset() { + currentLoginWizard = null + currentRegistrationWizard = null + + pendingSessionData = null + + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + pendingSessionStore.delete() + } + } + + override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, + credentials: Credentials, + callback: MatrixCallback<Session>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + createSessionFromSso(credentials, homeServerConnectionConfig) + } + } + + override fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, + callback: MatrixCallback<WellknownResult>): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId, homeServerConnectionConfig)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback<Session>): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + private suspend fun createSessionFromSso(credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { + sessionCreator.createSession(credentials, homeServerConnectionConfig) + } + + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { + val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b1c61e2721c784322226a09ae329e9dd0e6a1d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.internal.auth.db.PendingSessionData + +/** + * Store for elements when doing login or registration + */ +internal interface PendingSessionStore { + + suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) + + fun getPendingSessionData(): PendingSessionData? + + suspend fun delete() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt new file mode 100644 index 0000000000000000000000000000000000000000..a44cda5b57cfdf60c31963cd74d514ab4f827f7e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import android.net.Uri +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.SessionManager +import timber.log.Timber +import javax.inject.Inject + +internal interface SessionCreator { + suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session +} + +internal class DefaultSessionCreator @Inject constructor( + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager, + private val pendingSessionStore: PendingSessionStore +) : SessionCreator { + + /** + * Credentials can affect the homeServerConnectionConfig, override home server url and/or + * identity server url if provided in the credentials + */ + override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + // We can cleanup the pending session params + pendingSessionStore.delete() + + val sessionParams = SessionParams( + credentials = credentials, + homeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL + // remove trailing "/" + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + ?.also { Timber.d("Overriding homeserver url to $it") } + ?.let { Uri.parse(it) } + ?: homeServerConnectionConfig.homeServerUri, + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL + // remove trailing "/" + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + ?.also { Timber.d("Overriding identity server url to $it") } + ?.let { Uri.parse(it) } + ?: homeServerConnectionConfig.identityServerUri + ), + isTokenValid = true) + + sessionParamsStore.save(sessionParams) + return sessionManager.getOrCreateSession(sessionParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb038ecffba61b1f2549902cdd6cb8a640982e3d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams + +internal interface SessionParamsStore { + + fun get(sessionId: String): SessionParams? + + fun getLast(): SessionParams? + + fun getAll(): List<SessionParams> + + suspend fun save(sessionParams: SessionParams) + + suspend fun setTokenInvalid(sessionId: String) + + suspend fun updateCredentials(newCredentials: Credentials) + + suspend fun delete(sessionId: String) + + suspend fun deleteAll() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a631a567767d1b38174f5dc82727862153a4c57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * An interactive authentication flow. + */ +@JsonClass(generateAdapter = true) +data class InteractiveAuthenticationFlow( + + @Json(name = "type") + val type: String? = null, + + @Json(name = "stages") + val stages: List<String>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..9fb7eb5f3a838622f2d0397500dcde5ef1738610 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class LoginFlowResponse( + /** + * The homeserver's supported login types + */ + @Json(name = "flows") + val flows: List<LoginFlow>? +) + +@JsonClass(generateAdapter = true) +internal data class LoginFlow( + /** + * The login type. This is supplied as the type when logging in. + */ + @Json(name = "type") + val type: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..fc7206779ea59aa000a51a1439bb5f26378951ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +internal interface LoginParams { + val type: String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..60eebea57d81af0960fef1303770eb1b4591f98e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * Ref: + * - https://matrix.org/docs/spec/client_server/r0.5.0#password-based + * - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types + */ +@JsonClass(generateAdapter = true) +internal data class PasswordLoginParams( + @Json(name = "identifier") val identifier: Map<String, String>, + @Json(name = "password") val password: String, + @Json(name = "type") override val type: String, + @Json(name = "initial_device_display_name") val deviceDisplayName: String?, + @Json(name = "device_id") val deviceId: String?) : LoginParams { + + companion object { + private const val IDENTIFIER_KEY_TYPE = "type" + + private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user" + private const val IDENTIFIER_KEY_USER = "user" + + private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty" + private const val IDENTIFIER_KEY_MEDIUM = "medium" + private const val IDENTIFIER_KEY_ADDRESS = "address" + + private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone" + private const val IDENTIFIER_KEY_COUNTRY = "country" + private const val IDENTIFIER_KEY_PHONE = "phone" + + fun userIdentifier(user: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER, + IDENTIFIER_KEY_USER to user + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + + fun thirdPartyIdentifier(medium: String, + address: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY, + IDENTIFIER_KEY_MEDIUM to medium, + IDENTIFIER_KEY_ADDRESS to address + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + + fun phoneIdentifier(country: String, + phone: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE, + IDENTIFIER_KEY_COUNTRY to country, + IDENTIFIER_KEY_PHONE to phone + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..42db3152620f85f7d140c6b457390b96a0dc84d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RiotConfig( + // There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting + // Ex: "brand", "branding", etc. + @Json(name = "default_hs_url") + val defaultHomeServerUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt new file mode 100644 index 0000000000000000000000000000000000000000..d47eca8c9fb7b253f2bd20a5b67a395bbbc1e443 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +internal object ThreePidMedium { + const val EMAIL = "email" + const val MSISDN = "msisdn" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..3d9f58f0485cbb6e7013b3db75ec599028ad80c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +@JsonClass(generateAdapter = true) +internal data class TokenLoginParams( + @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, + @Json(name = "token") val token: String +) : LoginParams diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt new file mode 100644 index 0000000000000000000000000000000000000000..88e280479811e0e3445623aae900275570acad83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber + +internal object AuthRealmMigration : RealmMigration { + + // Current schema version + const val SCHEMA_VERSION = 3L + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create PendingSessionEntity") + + realm.schema.create("PendingSessionEntity") + .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) + .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) + .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) + .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) + .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) + .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) + .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) + .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) + .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId()) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..282d0df75df3747fccb6219b5f01b62cd9cc95b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for authentication classes + */ +@RealmModule(library = true, + classes = [ + SessionParamsEntity::class, + PendingSessionEntity::class + ]) +internal class AuthRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt new file mode 100644 index 0000000000000000000000000000000000000000..ad51f63ee80074ba2a0a6cac62baa22361179fde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.login.ResetPasswordData +import org.matrix.android.sdk.internal.auth.registration.ThreePidData +import java.util.UUID + +/** + * This class holds all pending data when creating a session, either by login or by register + */ +internal data class PendingSessionData( + val homeServerConnectionConfig: HomeServerConnectionConfig, + + /* ========================================================================================== + * Common + * ========================================================================================== */ + + val clientSecret: String = UUID.randomUUID().toString(), + val sendAttempt: Int = 0, + + /* ========================================================================================== + * For login + * ========================================================================================== */ + + val resetPasswordData: ResetPasswordData? = null, + + /* ========================================================================================== + * For register + * ========================================================================================== */ + + val currentSession: String? = null, + val isRegistrationStarted: Boolean = false, + val currentThreePidData: ThreePidData? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ee342d02c070c0caaceacd5fb031d6b6cbaedc7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.RealmObject + +internal open class PendingSessionEntity( + var homeServerConnectionConfigJson: String = "", + var clientSecret: String = "", + var sendAttempt: Int = 0, + var resetPasswordDataJson: String? = null, + var currentSession: String? = null, + var isRegistrationStarted: Boolean = false, + var currentThreePidDataJson: String? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..d357221f82f68806f84ef2daccc7fc9e21331163 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.login.ResetPasswordData +import org.matrix.android.sdk.internal.auth.registration.ThreePidData +import javax.inject.Inject + +internal class PendingSessionMapper @Inject constructor(moshi: Moshi) { + + private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java) + private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java) + private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java) + + fun map(entity: PendingSessionEntity?): PendingSessionData? { + if (entity == null) { + return null + } + + val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!! + val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) } + val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) } + + return PendingSessionData( + homeServerConnectionConfig = homeServerConnectionConfig, + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + resetPasswordData = resetPasswordData, + currentSession = entity.currentSession, + isRegistrationStarted = entity.isRegistrationStarted, + currentThreePidData = threePidData) + } + + fun map(sessionData: PendingSessionData?): PendingSessionEntity? { + if (sessionData == null) { + return null + } + + val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig) + val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData) + val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData) + + return PendingSessionEntity( + homeServerConnectionConfigJson = homeServerConnectionConfigJson, + clientSecret = sessionData.clientSecret, + sendAttempt = sessionData.sendAttempt, + resetPasswordDataJson = resetPasswordDataJson, + currentSession = sessionData.currentSession, + isRegistrationStarted = sessionData.isRegistrationStarted, + currentThreePidDataJson = currentThreePidDataJson + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..41851fc2c6e427fb9631eb34d22d45125f036cf2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.di.AuthDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper, + @AuthDatabase + private val realmConfiguration: RealmConfiguration +) : PendingSessionStore { + + override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) { + awaitTransaction(realmConfiguration) { realm -> + val entity = mapper.map(pendingSessionData) + if (entity != null) { + realm.where(PendingSessionEntity::class.java) + .findAll() + .deleteAllFromRealm() + + realm.insert(entity) + } + } + } + + override fun getPendingSessionData(): PendingSessionData? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(PendingSessionEntity::class.java) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + } + } + + override suspend fun delete() { + awaitTransaction(realmConfiguration) { + it.where(PendingSessionEntity::class.java) + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..57f1c23e99f968d2bb9d784e30169c6475a075c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.di.AuthDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.exceptions.RealmPrimaryKeyConstraintException +import timber.log.Timber +import javax.inject.Inject + +internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper, + @AuthDatabase + private val realmConfiguration: RealmConfiguration +) : SessionParamsStore { + + override fun getLast(): SessionParams? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .findAll() + .map { mapper.map(it) } + .lastOrNull() + } + } + + override fun get(sessionId: String): SessionParams? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + } + } + + override fun getAll(): List<SessionParams> { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .findAll() + .mapNotNull { mapper.map(it) } + } + } + + override suspend fun save(sessionParams: SessionParams) { + awaitTransaction(realmConfiguration) { + val entity = mapper.map(sessionParams) + if (entity != null) { + try { + it.insert(entity) + } catch (e: RealmPrimaryKeyConstraintException) { + Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials") + it.insertOrUpdate(entity) + } + } + } + } + + override suspend fun setTokenInvalid(sessionId: String) { + awaitTransaction(realmConfiguration) { realm -> + val currentSessionParams = realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .firstOrNull() + + if (currentSessionParams == null) { + // Should not happen + "Session param not found for id $sessionId" + .let { Timber.w(it) } + .also { error(it) } + } else { + currentSessionParams.isTokenValid = false + } + } + } + + override suspend fun updateCredentials(newCredentials: Credentials) { + awaitTransaction(realmConfiguration) { realm -> + val currentSessionParams = realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, newCredentials.sessionId()) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + + if (currentSessionParams == null) { + // Should not happen + "Session param not found for id ${newCredentials.sessionId()}" + .let { Timber.w(it) } + .also { error(it) } + } else { + val newSessionParams = currentSessionParams.copy( + credentials = newCredentials, + isTokenValid = true + ) + + val entity = mapper.map(newSessionParams) + if (entity != null) { + realm.insertOrUpdate(entity) + } + } + } + } + + override suspend fun delete(sessionId: String) { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .deleteAllFromRealm() + } + } + + override suspend fun deleteAll() { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..81202d2f52de1791b25b099ac5591bb5f6be5488 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class SessionParamsEntity( + @PrimaryKey var sessionId: String = "", + var userId: String = "", + var credentialsJson: String = "", + var homeServerConnectionConfigJson: String = "", + // Set to false when the token is invalid and the user has been soft logged out + // In case of hard logout, this object is deleted from DB + var isTokenValid: Boolean = true +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..78324b6916c24235da9014b667858e4b40f01093 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.db + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import javax.inject.Inject + +internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { + + private val credentialsAdapter = moshi.adapter(Credentials::class.java) + private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java) + + fun map(entity: SessionParamsEntity?): SessionParams? { + if (entity == null) { + return null + } + val credentials = credentialsAdapter.fromJson(entity.credentialsJson) + val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson) + if (credentials == null || homeServerConnectionConfig == null) { + return null + } + return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid) + } + + fun map(sessionParams: SessionParams?): SessionParamsEntity? { + if (sessionParams == null) { + return null + } + val credentialsJson = credentialsAdapter.toJson(sessionParams.credentials) + val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionParams.homeServerConnectionConfig) + if (credentialsJson == null || homeServerConnectionConfigJson == null) { + return null + } + return SessionParamsEntity( + sessionParams.credentials.sessionId(), + sessionParams.userId, + credentialsJson, + homeServerConnectionConfigJson, + sessionParams.isTokenValid) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt new file mode 100644 index 0000000000000000000000000000000000000000..71b8f6406993d41ab6764e85c2f17cb3715e18b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import android.util.Patterns +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.auth.data.ThreePidMedium +import org.matrix.android.sdk.internal.auth.data.TokenLoginParams +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse +import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient + +internal class DefaultLoginWizard( + okHttpClient: OkHttpClient, + retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope +) : LoginWizard { + + private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + + private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + override fun login(login: String, + password: String, + deviceName: String, + callback: MatrixCallback<Session>): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + loginInternal(login, password, deviceName) + } + } + + /** + * Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint + */ + override fun loginWithToken(loginToken: String, callback: MatrixCallback<Session>): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val loginParams = TokenLoginParams( + token = loginToken + ) + val credentials = executeRequest<Credentials>(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + } + + private suspend fun loginInternal(login: String, + password: String, + deviceName: String) = withContext(coroutineDispatchers.computation) { + val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { + PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName) + } else { + PasswordLoginParams.userIdentifier(login, password, deviceName) + } + val credentials = executeRequest<Credentials>(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + + override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + resetPasswordInternal(email, newPassword) + } + } + + private suspend fun resetPasswordInternal(email: String, newPassword: String) { + val param = RegisterAddThreePidTask.Params( + RegisterThreePid.Email(email), + pendingSessionData.clientSecret, + pendingSessionData.sendAttempt + ) + + pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) + .also { pendingSessionStore.savePendingSessionData(it) } + + val result = executeRequest<AddThreePidRegistrationResponse>(null) { + apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) + } + + pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result)) + .also { pendingSessionStore.savePendingSessionData(it) } + } + + override fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable { + val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run { + callback.onFailure(IllegalStateException("developer error, no reset password in progress")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + resetPasswordMailConfirmedInternal(safeResetPasswordData) + } + } + + private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) { + val param = ResetPasswordMailConfirmed.create( + pendingSessionData.clientSecret, + resetPasswordData.addThreePidRegistrationResponse.sid, + resetPasswordData.newPassword + ) + + executeRequest<Unit>(null) { + apiCall = authAPI.resetPasswordMailConfirmed(param) + } + + // Set to null? + // resetPasswordData = null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f759dc4235f8299922d7706c3002f226100a20ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import dagger.Lazy +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task<DirectLoginTask.Params, Session> { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val client = buildClient(params.homeServerConnectionConfig) + val homeServerUrl = params.homeServerConnectionConfig.homeServerUri.toString() + + val authAPI = retrofitFactory.create(client, homeServerUrl) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = try { + executeRequest<Credentials>(null) { + apiCall = authAPI.login(loginParams) + } + } catch (throwable: Throwable) { + when (throwable) { + is UnrecognizedCertificateException -> { + throw Failure.UnrecognizedCertificateFailure( + homeServerUrl, + throwable.fingerprint + ) + } + else -> + throw throwable + } + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6f621c2dbdeedfb218d3de67d38012818c82473 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse + +/** + * Container to store the data when a reset password is in the email validation step + */ +@JsonClass(generateAdapter = true) +internal data class ResetPasswordData( + val newPassword: String, + val addThreePidRegistrationResponse: AddThreePidRegistrationResponse +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt new file mode 100644 index 0000000000000000000000000000000000000000..c291c7888262d483173cb7c73a865ed72e341fe4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.auth.login + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.auth.registration.AuthParams + +/** + * Class to pass parameters to reset the password once a email has been validated. + */ +@JsonClass(generateAdapter = true) +internal data class ResetPasswordMailConfirmed( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the new password + @Json(name = "new_password") + val newPassword: String? = null +) { + companion object { + fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed { + return ResetPasswordMailConfirmed( + auth = AuthParams.createForResetPassword(clientSecret, sid), + newPassword = newPassword + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..7fbdaacb81f55de699466e6d1a9ea879c482c4ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid + +/** + * Add a three Pid during authentication + */ +@JsonClass(generateAdapter = true) +internal data class AddThreePidRegistrationParams( + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed 255 characters and it must not be empty. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most recent one which it has seen, + * scoped to that email + client_secret pair. This is to avoid repeatedly sending the same email in the case of request retries between + * the POSTing user and the identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + val sendAttempt: Int, + + /** + * Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when + * submitting 3PID validation information through a POST request. + */ + @Json(name = "next_link") + val nextLink: String? = null, + + /** + * Required. The hostname of the identity server to communicate with. May optionally include a port. + * This parameter is ignored when the homeserver handles 3PID verification. + */ + @Json(name = "id_server") + val id_server: String? = null, + + /* ========================================================================================== + * For emails + * ========================================================================================== */ + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String? = null, + + /* ========================================================================================== + * For Msisdn + * ========================================================================================== */ + + /** + * Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String? = null, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val msisdn: String? = null +) { + companion object { + fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams { + return when (params.threePid) { + is RegisterThreePid.Email -> AddThreePidRegistrationParams( + email = params.threePid.email, + clientSecret = params.clientSecret, + sendAttempt = params.sendAttempt + ) + is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams( + msisdn = params.threePid.msisdn, + countryCode = params.threePid.countryCode, + clientSecret = params.clientSecret, + sendAttempt = params.sendAttempt + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d60724e99d5cff780192b698ec1e5ff743b5dcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddThreePidRegistrationResponse( + /** + * Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String, + + /** + * An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity + * Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable), + * who should then be prompted to provide it to the client. + * + * If this field is not present, the client can assume that verification will happen without the client's involvement provided + * the homeserver advertises this specification version in the /versions response (ie: r0.5.0). + */ + @Json(name = "submit_url") + val submitUrl: String? = null, + + /* ========================================================================================== + * It seems that the homeserver is sending more data, we may need it + * ========================================================================================== */ + + @Json(name = "msisdn") + val msisdn: String? = null, + + @Json(name = "intl_fmt") + val formattedMsisdn: String? = null, + + @Json(name = "success") + val success: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3136526da8041a046f8fc77d3adc041ed5b2eed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * Open class, parent to all possible authentication parameters + */ +@JsonClass(generateAdapter = true) +internal data class AuthParams( + @Json(name = "type") + val type: String, + + /** + * Note: session can be null for reset password request + */ + @Json(name = "session") + val session: String?, + + /** + * parameter for "m.login.recaptcha" type + */ + @Json(name = "response") + val captchaResponse: String? = null, + + /** + * parameter for "m.login.email.identity" type + */ + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials? = null +) { + + companion object { + fun createForCaptcha(session: String, captchaResponse: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.RECAPTCHA, + session = session, + captchaResponse = captchaResponse + ) + } + + fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = session, + threePidCredentials = threePidCredentials + ) + } + + /** + * Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN, + * the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401. + */ + fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.MSISDN, + session = session, + threePidCredentials = threePidCredentials + ) + } + + fun createForResetPassword(clientSecret: String, sid: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = null, + threePidCredentials = ThreePidCredentials( + clientSecret = clientSecret, + sid = sid + ) + ) + } + } +} + +@JsonClass(generateAdapter = true) +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + @Json(name = "sid") + val sid: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt new file mode 100644 index 0000000000000000000000000000000000000000..79b71b208e6aa685e5a4abc93743e3e93a01e614 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import okhttp3.OkHttpClient + +/** + * This class execute the registration request and is responsible to keep the session of interactive authentication + */ +internal class DefaultRegistrationWizard( + private val okHttpClient: OkHttpClient, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope +) : RegistrationWizard { + + private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + + private val authAPI = buildAuthAPI() + private val registerTask = DefaultRegisterTask(authAPI) + private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) + private val validateCodeTask = DefaultValidateCodeTask(authAPI) + + override val currentThreePid: String? + get() { + return when (val threePid = pendingSessionData.currentThreePidData?.threePid) { + is RegisterThreePid.Email -> threePid.email + is RegisterThreePid.Msisdn -> { + // Take formatted msisdn if provided by the server + pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn + } + null -> null + } + } + + override val isRegistrationStarted: Boolean + get() = pendingSessionData.isRegistrationStarted + + override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable { + val params = RegistrationParams() + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun createAccount(userName: String, + password: String, + initialDeviceDisplayName: String?, + callback: MatrixCallback<RegistrationResult>): Cancelable { + val params = RegistrationParams( + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName + ) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + .also { + pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) + .also { pendingSessionStore.savePendingSessionData(it) } + } + } + } + + override fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + pendingSessionData = pendingSessionData.copy(currentThreePidData = null) + .also { pendingSessionStore.savePendingSessionData(it) } + + sendThreePid(threePid) + } + } + + override fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable { + val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + sendThreePid(safeCurrentThreePid) + } + } + + private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult { + val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first") + val response = registerAddThreePidTask.execute( + RegisterAddThreePidTask.Params( + threePid, + pendingSessionData.clientSecret, + pendingSessionData.sendAttempt)) + + pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) + .also { pendingSessionStore.savePendingSessionData(it) } + + val params = RegistrationParams( + auth = if (threePid is RegisterThreePid.Email) { + AuthParams.createForEmailIdentity(safeSession, + ThreePidCredentials( + clientSecret = pendingSessionData.clientSecret, + sid = response.sid + ) + ) + } else { + AuthParams.createForMsisdnIdentity(safeSession, + ThreePidCredentials( + clientSecret = pendingSessionData.clientSecret, + sid = response.sid + ) + ) + } + ) + // Store data + pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) + .also { pendingSessionStore.savePendingSessionData(it) } + + // and send the sid a first time + return performRegistrationRequest(params) + } + + override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable { + val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run { + callback.onFailure(IllegalStateException("developer error, no pending three pid")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(safeParam, delayMillis) + } + } + + override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + validateThreePid(code) + } + } + + private suspend fun validateThreePid(code: String): RegistrationResult { + val registrationParams = pendingSessionData.currentThreePidData?.registrationParams + ?: throw IllegalStateException("developer error, no pending three pid") + val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") + val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code") + val validationBody = ValidationCodeBody( + clientSecret = pendingSessionData.clientSecret, + sid = safeCurrentData.addThreePidRegistrationResponse.sid, + code = code + ) + val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) + if (validationResponse.isSuccess()) { + // The entered code is correct + // Same than validate email + return performRegistrationRequest(registrationParams, 3_000) + } else { + // The code is not correct + throw Failure.SuccessError + } + } + + override fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) + performRegistrationRequest(params) + } + } + + private suspend fun performRegistrationRequest(registrationParams: RegistrationParams, + delayMillis: Long = 0): RegistrationResult { + delay(delayMillis) + val credentials = try { + registerTask.execute(RegisterTask.Params(registrationParams)) + } catch (exception: Throwable) { + if (exception is RegistrationFlowError) { + pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session) + .also { pendingSessionStore.savePendingSessionData(it) } + return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult()) + } else { + throw exception + } + } + + val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + return RegistrationResult.Success(session) + } + + private fun buildAuthAPI(): AuthAPI { + val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt new file mode 100644 index 0000000000000000000000000000000000000000..45e2f80fcc04d96e9d7996c59fc8e8f10da38101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@Parcelize +data class LocalizedFlowDataLoginTerms( + var policyName: String? = null, + var version: String? = null, + var localizedUrl: String? = null, + var localizedName: String? = null +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ad15822cac7fbbcd0c53f226102e1c37fcdbcb9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface RegisterAddThreePidTask : Task<RegisterAddThreePidTask.Params, AddThreePidRegistrationResponse> { + data class Params( + val threePid: RegisterThreePid, + val clientSecret: String, + val sendAttempt: Int + ) +} + +internal class DefaultRegisterAddThreePidTask( + private val authAPI: AuthAPI +) : RegisterAddThreePidTask { + + override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse { + return executeRequest(null) { + apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params)) + } + } + + private fun RegisterThreePid.toPath(): String { + return when (this) { + is RegisterThreePid.Email -> "email" + is RegisterThreePid.Msisdn -> "msisdn" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b3924138ec27ca2d261d846e6a9de4719506663 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface RegisterTask : Task<RegisterTask.Params, Credentials> { + data class Params( + val registrationParams: RegistrationParams + ) +} + +internal class DefaultRegisterTask( + private val authAPI: AuthAPI +) : RegisterTask { + + override suspend fun execute(params: RegisterTask.Params): Credentials { + try { + return executeRequest(null) { + apiCall = authAPI.register(params.registrationParams) + } + } catch (throwable: Throwable) { + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..267e50eeb9f2c553e2c6cf57afb2869d95f5f5b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.auth.registration.TermPolicies +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow + +@JsonClass(generateAdapter = true) +data class RegistrationFlowResponse( + + /** + * The list of flows. + */ + @Json(name = "flows") + val flows: List<InteractiveAuthenticationFlow>? = null, + + /** + * The list of stages the client has completed successfully. + */ + @Json(name = "completed") + val completedStages: List<String>? = null, + + /** + * The session identifier that the client must pass back to the home server, if one is provided, + * in subsequent attempts to authenticate in the same API call. + */ + @Json(name = "session") + val session: String? = null, + + /** + * The information that the client will need to know in order to use a given type of authentication. + * For each login stage type presented, that type may be present as a key in this dictionary. + * For example, the public key of reCAPTCHA stage could be given here. + */ + @Json(name = "params") + val params: JsonDict? = null + + /** + * WARNING, + * The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage, + * But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure] + * Ex: when polling for "m.login.msisdn" validation + */ +) + +/** + * Convert to something easier to handle on client side + */ +fun RegistrationFlowResponse.toFlowResult(): FlowResult { + // Get all the returned stages + val allFlowTypes = mutableSetOf<String>() + + val missingStage = mutableListOf<Stage>() + val completedStage = mutableListOf<Stage>() + + this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } + + allFlowTypes.forEach { type -> + val isMandatory = flows?.all { type in it.stages.orEmpty() } == true + + val stage = when (type) { + LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) + ?: "") + LoginFlowTypes.DUMMY -> Stage.Dummy(isMandatory) + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap<String, String>()) + LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) + LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) + else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) + } + + if (type in completedStages.orEmpty()) { + completedStage.add(stage) + } else { + missingStage.add(stage) + } + } + + return FlowResult(missingStage, completedStage) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..4089e280d749aecfddc3bb8c11865dfb4bbdb18d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to the different registration types for /register. + */ +@JsonClass(generateAdapter = true) +internal data class RegistrationParams( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the account username + @Json(name = "username") + val username: String? = null, + + // the account password + @Json(name = "password") + val password: String? = null, + + // device name + @Json(name = "initial_device_display_name") + val initialDeviceDisplayName: String? = null, + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + val x_show_msisdn: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..bfebc578843575efd22dee5bd38f145dda4b9aab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse + +@JsonClass(generateAdapter = true) +data class SuccessResult( + @Json(name = "success") + val success: Boolean? +) { + fun isSuccess() = success.orFalse() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt new file mode 100644 index 0000000000000000000000000000000000000000..25a7fa3ab2f0497d0c1dd3b60a0cd98e4cef38d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid + +/** + * Container to store the data when a three pid is in validation step + */ +@JsonClass(generateAdapter = true) +internal data class ThreePidData( + val email: String, + val msisdn: String, + val country: String, + val addThreePidRegistrationResponse: AddThreePidRegistrationResponse, + val registrationParams: RegistrationParams +) { + val threePid: RegisterThreePid + get() { + return if (email.isNotBlank()) { + RegisterThreePid.Email(email) + } else { + RegisterThreePid.Msisdn(msisdn, country) + } + } + + companion object { + fun from(threePid: RegisterThreePid, + addThreePidRegistrationResponse: AddThreePidRegistrationResponse, + registrationParams: RegistrationParams): ThreePidData { + return when (threePid) { + is RegisterThreePid.Email -> + ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams) + is RegisterThreePid.Msisdn -> + ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..470faae7103949bb82beb6f7173f990761ffaab5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface ValidateCodeTask : Task<ValidateCodeTask.Params, SuccessResult> { + data class Params( + val url: String, + val body: ValidationCodeBody + ) +} + +internal class DefaultValidateCodeTask( + private val authAPI: AuthAPI +) : ValidateCodeTask { + + override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult { + return executeRequest(null) { + apiCall = authAPI.validate3Pid(params.url, params.body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..ad4a3d46091c66ea7a83af1ed4d10e27a510961e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This object is used to send a code received by SMS to validate Msisdn ownership + */ +@JsonClass(generateAdapter = true) +data class ValidationCodeBody( + @Json(name = "client_secret") + val clientSecret: String, + + @Json(name = "sid") + val sid: String, + + @Json(name = "token") + val code: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a02bc62e96c71cfbcd5c4927575f0f95e8eb3f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.version + +/** + * Values will take the form "rX.Y.Z". + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions + */ +internal data class HomeServerVersion( + val major: Int, + val minor: Int, + val patch: Int +) : Comparable<HomeServerVersion> { + override fun compareTo(other: HomeServerVersion): Int { + return when { + major > other.major -> 1 + major < other.major -> -1 + minor > other.minor -> 1 + minor < other.minor -> -1 + patch > other.patch -> 1 + patch < other.patch -> -1 + else -> 0 + } + } + + companion object { + internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""") + + internal fun parse(value: String): HomeServerVersion? { + val result = pattern.matchEntire(value) ?: return null + return HomeServerVersion( + major = result.groupValues[1].toInt(), + minor = result.groupValues[2].toInt(), + patch = result.groupValues[3].toInt() + ) + } + + val r0_0_0 = HomeServerVersion(major = 0, minor = 0, patch = 0) + val r0_1_0 = HomeServerVersion(major = 0, minor = 1, patch = 0) + val r0_2_0 = HomeServerVersion(major = 0, minor = 2, patch = 0) + val r0_3_0 = HomeServerVersion(major = 0, minor = 3, patch = 0) + val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0) + val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0) + val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt new file mode 100644 index 0000000000000000000000000000000000000000..483c43f5022824e00ac039370727e0ce36d9740a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.version + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions + * + * Ex: + * <pre> + * { + * "unstable_features": { + * "m.lazy_load_members": true + * }, + * "versions": [ + * "r0.0.1", + * "r0.1.0", + * "r0.2.0", + * "r0.3.0" + * ] + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +internal data class Versions( + @Json(name = "versions") + val supportedVersions: List<String>? = null, + + @Json(name = "unstable_features") + val unstableFeatures: Map<String, Boolean>? = null +) + +// MatrixVersionsFeature +private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members" +private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server" +private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" +private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" + +/** + * Return true if the SDK supports this homeserver version + */ +internal fun Versions.isSupportedBySdk(): Boolean { + return supportLazyLoadMembers() +} + +/** + * Return true if the SDK supports this homeserver version for login and registration + */ +internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { + return !doesServerRequireIdentityServerParam() + && doesServerAcceptIdentityAccessToken() + && doesServerSeparatesAddAndBind() +} + +/** + * Return true if the server support the lazy loading of room members + * + * @return true if the server support the lazy loading of room members + */ +private fun Versions.supportLazyLoadMembers(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_5_0 + || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true +} + +/** + * Indicate if the `id_server` parameter is required when registering with an 3pid, + * adding a 3pid or resetting password. + */ +private fun Versions.doesServerRequireIdentityServerParam(): Boolean { + if (getMaxVersion() >= HomeServerVersion.r0_6_0) return false + return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true +} + +/** + * Indicate if the `id_access_token` parameter can be safely passed to the homeserver. + * Some homeservers may trigger errors if they are not prepared for the new parameter. + */ +private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_6_0 + || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false +} + +private fun Versions.doesServerSeparatesAddAndBind(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_6_0 + || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false +} + +private fun Versions.getMaxVersion(): HomeServerVersion { + return supportedVersions + ?.mapNotNull { HomeServerVersion.parse(it) } + ?.max() + ?: HomeServerVersion.r0_0_0 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4f55d14bf59ca6fbfff7071db43fabb8cf64447 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class CancelGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val requestId: String, + val recipients: Map<String, List<String>> + ) { + companion object { + fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { + return Params( + sessionId = sessionId, + requestId = request.requestId, + recipients = request.recipients + ) + } + } + } + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap<Any>() + val toDeviceContent = ShareRequestCancellation( + requestingDeviceId = credentials.deviceId, + requestId = params.requestId + ) + cryptoStore.saveGossipingEvent(Event( + type = EventType.ROOM_KEY_REQUEST, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + try { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c8b525b9699d553d2926e32498897edf4ea89ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +/** + * Matrix algorithm value for olm. + */ +const val MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2" + +/** + * Matrix algorithm value for megolm. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2" + +/** + * Matrix algorithm value for megolm keys backup. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" + +/** + * Secured Shared Storage algorithm constant + */ +const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2" +/* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. **/ +const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2" + +// TODO Refacto: use this constants everywhere +const val ed25519 = "ed25519" +const val curve25519 = "curve25519" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5496a6fd151b65df127d8edb564ce99fdbbd0ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.crosssigning.ComputeTrustTask +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultComputeTrustTask +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers +import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask +import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.cache.ClearCacheTask +import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask +import io.realm.RealmConfiguration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import retrofit2.Retrofit +import java.io.File + +@Module +internal abstract class CryptoModule { + + @Module + companion object { + internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5" + + @JvmStatic + @Provides + @CryptoDatabase + @SessionScope + fun providesRealmConfiguration(@SessionFilesDirectory directory: File, + @UserMd5 userMd5: String, + realmCryptoStoreMigration: RealmCryptoStoreMigration, + realmKeysUtils: RealmKeysUtils): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .apply { + realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) + } + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(realmCryptoStoreMigration) + .build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesCryptoCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob()) + } + + @JvmStatic + @Provides + @CryptoDatabase + fun providesClearCacheTask(@CryptoDatabase + realmConfiguration: RealmConfiguration): ClearCacheTask { + return RealmClearCacheTask(realmConfiguration) + } + + @JvmStatic + @Provides + @SessionScope + fun providesCryptoAPI(retrofit: Retrofit): CryptoApi { + return retrofit.create(CryptoApi::class.java) + } + + @JvmStatic + @Provides + @SessionScope + fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi { + return retrofit.create(RoomKeysApi::class.java) + } + } + + @Binds + abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService + + @Binds + abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask + + @Binds + abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask + + @Binds + abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask + + @Binds + abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask + + @Binds + abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask + + @Binds + abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask + + @Binds + abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask + + @Binds + abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask + + @Binds + abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask + + @Binds + abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask + + @Binds + abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask + + @Binds + abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask + + @Binds + abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask + + @Binds + abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask + + @Binds + abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask + + @Binds + abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask + + @Binds + abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask + + @Binds + abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask + + @Binds + abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask + + @Binds + abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask + + @Binds + abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask + + @Binds + abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask + + @Binds + abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask + + @Binds + abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask + + @Binds + abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask + + @Binds + abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask + + @Binds + abstract fun bindDeleteDeviceWithUserPasswordTask(task: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask + + @Binds + abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService + + @Binds + abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore + + @Binds + abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask + + @Binds + abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask + + @Binds + abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt new file mode 100755 index 0000000000000000000000000000000000000000..f8fb5a35d00423c242d7763d53347850aebde545 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -0,0 +1,1360 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import com.squareup.moshi.Types +import com.zhuinden.monarchy.Monarchy +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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 +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.toRest +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.fetchCopied +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.matrix.olm.OlmManager +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject +import kotlin.math.max + +/** + * A `CryptoService` class instance manages the end-to-end crypto for a session. + * + * + * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted + * before sending. + * In the other hand, received events goes through CryptoService for decrypting. + * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. + * Specially, it tracks all room membership changes events in order to do keys updates. + */ +@SessionScope +internal class DefaultCryptoService @Inject constructor( + // Olm Manager + private val olmManager: OlmManager, + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, + // the crypto store + private val cryptoStore: IMXCryptoStore, + // Room encryptors store + private val roomEncryptorsStore: RoomEncryptorsStore, + // Olm device + private val olmDevice: MXOlmDevice, + // Set of parameters used to configure/customize the end-to-end crypto. + private val mxCryptoConfig: MXCryptoConfig, + // Device list manager + private val deviceListManager: DeviceListManager, + // The key backup service. + private val keysBackupService: DefaultKeysBackupService, + // + private val objectSigner: ObjectSigner, + // + private val oneTimeKeysUploader: OneTimeKeysUploader, + // + private val roomDecryptorProvider: RoomDecryptorProvider, + // The verification service. + private val verificationService: DefaultVerificationService, + + private val crossSigningService: DefaultCrossSigningService, + // + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + // + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + // Actions + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val megolmSessionDataImporter: MegolmSessionDataImporter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + // Repository + private val megolmEncryptionFactory: MXMegolmEncryptionFactory, + private val olmEncryptionFactory: MXOlmEncryptionFactory, + private val deleteDeviceTask: DeleteDeviceTask, + private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, + // Tasks + private val getDevicesTask: GetDevicesTask, + private val getDeviceInfoTask: GetDeviceInfoTask, + private val setDeviceNameTask: SetDeviceNameTask, + private val uploadKeysTask: UploadKeysTask, + private val loadRoomMembersTask: LoadRoomMembersTask, + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor, + private val cryptoCoroutineScope: CoroutineScope, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter +) : CryptoService { + + init { + verificationService.cryptoService = this + } + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val isStarting = AtomicBoolean(false) + private val isStarted = AtomicBoolean(false) + + // The date of the last time we forced establishment + // of a new session for each user:device. + private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>() + + fun onStateEvent(roomId: String, event: Event) { + when { + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } + } + + fun onLiveEvent(roomId: String, event: Event) { + when { + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } + } + + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) { + setDeviceNameTask + .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { + this.executionThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + // bg refresh of crypto device + downloadKeys(listOf(userId), true, NoOpMatrixCallback()) + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) { + deleteDeviceTask + .configureWith(DeleteDeviceTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) { + deleteDeviceWithUserPasswordTask + .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getCryptoVersion(context: Context, longFormat: Boolean): String { + return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version + } + + override fun getMyDevice(): CryptoDeviceInfo { + return myDeviceInfoHolder.get().myDevice + } + + override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) { + getDevicesTask + .configureWith { + // this.executionThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback<DevicesListResponse> { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: DevicesListResponse) { + // Save in local DB + cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) + callback.onSuccess(data) + } + } + } + .executeBy(taskExecutor) + } + + override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> { + return cryptoStore.getLiveMyDevicesInfo() + } + + override fun getMyDevicesInfo(): List<DeviceInfo> { + return cryptoStore.getMyDevicesInfo() + } + + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) + } + + /** + * Provides the tracking status + * + * @param userId the user id + * @return the tracking status + */ + override fun getDeviceTrackingStatus(userId: String): Int { + return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED) + } + + /** + * Tell if the MXCrypto is started + * + * @return true if the crypto is started + */ + fun isStarted(): Boolean { + return isStarted.get() + } + + /** + * Tells if the MXCrypto is starting. + * + * @return true if the crypto is starting + */ + fun isStarting(): Boolean { + return isStarting.get() + } + + /** + * Start the crypto module. + * Device keys will be uploaded, then one time keys if there are not enough on the homeserver + * and, then, if this is the first time, this new device will be announced to all other users + * devices. + * + */ + fun start() { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + internalStart() + } + // Just update + fetchDevicesList(NoOpMatrixCallback()) + } + + fun ensureDevice() { + cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) { + // Open the store + cryptoStore.open() + // this can throw if no network + tryThis { + uploadDeviceKeys() + } + + oneTimeKeysUploader.maybeUploadOneTimeKeys() + // this can throw if no backup + tryThis { + keysBackupService.checkAndStartKeysBackup() + } + } + } + + fun onSyncWillProcess(isInitialSync: Boolean) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + if (isInitialSync) { + try { + // On initial sync, we start all our tracking from + // scratch, so mark everything as untracked. onCryptoEvent will + // be called for all e2e rooms during the processing of the sync, + // at which point we'll start tracking all the users of that room. + deviceListManager.invalidateAllDeviceLists() + // always track my devices? + deviceListManager.startTrackingDeviceList(listOf(userId)) + deviceListManager.refreshOutdatedDeviceLists() + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO onSyncWillProcess ") + } + } + } + } + + private fun internalStart() { + if (isStarted.get() || isStarting.get()) { + return + } + isStarting.set(true) + + // Open the store + cryptoStore.open() + + runCatching { +// if (isInitialSync) { +// // refresh the devices list for each known room members +// deviceListManager.invalidateAllDeviceLists() +// deviceListManager.refreshOutdatedDeviceLists() +// } else { + + // Why would we do that? it will be called at end of syn + incomingGossipingRequestManager.processReceivedGossipingRequests() +// } + }.fold( + { + isStarting.set(false) + isStarted.set(true) + }, + { + isStarting.set(false) + isStarted.set(false) + Timber.e(it, "Start failed") + } + ) + } + + /** + * Close the crypto + */ + fun close() = runBlocking(coroutineDispatchers.crypto) { + cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) + + olmDevice.release() + cryptoStore.close() + } + + // Aways enabled on RiotX + override fun isCryptoEnabled() = true + + /** + * @return the Keys backup Service + */ + override fun keysBackupService() = keysBackupService + + /** + * @return the VerificationService + */ + override fun verificationService() = verificationService + + override fun crossSigningService() = crossSigningService + + /** + * A sync response has been received + * + * @param syncResponse the syncResponse + */ + fun onSyncCompleted(syncResponse: SyncResponse) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + if (syncResponse.deviceLists != null) { + deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) + } + if (syncResponse.deviceOneTimeKeysCount != null) { + val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 + oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) + } + if (isStarted()) { + // Make sure we process to-device messages before generating new one-time-keys #2782 + deviceListManager.refreshOutdatedDeviceLists() + oneTimeKeysUploader.maybeUploadOneTimeKeys() + incomingGossipingRequestManager.processReceivedGossipingRequests() + } + } + } + } + + /** + * Find a device by curve25519 identity key + * + * @param senderKey the curve25519 key to match. + * @param algorithm the encryption algorithm. + * @return the device info, or null if not found / unsupported algorithm / crypto released + */ + override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? { + return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { + // We only deal in olm keys + null + } else cryptoStore.deviceWithIdentityKey(senderKey) + } + + /** + * Provides the device information for a user id and a device Id + * + * @param userId the user id + * @param deviceId the device id + */ + override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { + return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { + cryptoStore.getUserDevice(userId, deviceId) + } else { + null + } + } + + override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { + return cryptoStore.getUserDeviceList(userId).orEmpty() + } + + override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> { + return cryptoStore.getLiveDeviceList() + } + + override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> { + return cryptoStore.getLiveDeviceList(userId) + } + + override fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> { + return cryptoStore.getLiveDeviceList(userIds) + } + + /** + * Set the devices as known + * + * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. + * @param callback the asynchronous callback + */ + override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) { + // build a devices map + val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId }) + + for ((userId, deviceIds) in devicesIdListByUserId) { + val storedDeviceIDs = cryptoStore.getUserDevices(userId) + + // sanity checks + if (null != storedDeviceIDs) { + var isUpdated = false + + deviceIds.forEach { deviceId -> + val device = storedDeviceIDs[deviceId] + + // assume if the device is either verified or blocked + // it means that the device is known + if (device?.isUnknown == true) { + device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.storeUserDevices(userId, storedDeviceIDs) + } + } + } + + callback?.onSuccess(Unit) + } + + /** + * Update the blocked/verified state of the given device. + * + * @param trustLevel the new trust level + * @param userId the owner of the device + * @param deviceId the unique identifier for the device. + */ + override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + setDeviceVerificationAction.handle(trustLevel, userId, deviceId) + } + + /** + * Configure a room to use encryption. + * + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. + * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) + * @param membersId list of members to start tracking their devices + * @return true if the operation succeeds. + */ + private suspend fun setEncryptionInRoom(roomId: String, + algorithm: String?, + inhibitDeviceQuery: Boolean, + membersId: List<String>): Boolean { + // If we already have encryption in this room, we should ignore this event + // (for now at least. Maybe we should alert the user somehow?) + val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) + + if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { + Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + return false + } + + val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) + + if (!encryptingClass) { + Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + return false + } + + cryptoStore.storeRoomAlgorithm(roomId, algorithm!!) + + val alg: IMXEncrypting = when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) + else -> olmEncryptionFactory.create(roomId) + } + + roomEncryptorsStore.put(roomId, alg) + + // if encryption was not previously enabled in this room, we will have been + // ignoring new device events for these users so far. We may well have + // up-to-date lists for some users, for instance if we were sharing other + // e2e rooms with them, so there is room for optimisation here, but for now + // we just invalidate everyone in the room. + if (null == existingAlgorithm) { + Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") + + val userIds = ArrayList(membersId) + + deviceListManager.startTrackingDeviceList(userIds) + + if (!inhibitDeviceQuery) { + deviceListManager.refreshOutdatedDeviceLists() + } + } + + return true + } + + /** + * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM + * + * @param roomId the room id + * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM + */ + override fun isRoomEncrypted(roomId: String): Boolean { + val encryptionEvent = monarchy.fetchCopied { realm -> + EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .findFirst() + } + return encryptionEvent != null + } + + /** + * @return the stored device keys for a user. + */ + override fun getUserDevices(userId: String): MutableList<CryptoDeviceInfo> { + return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList() + } + + private fun isEncryptionEnabledForInvitedUser(): Boolean { + return mxCryptoConfig.enableEncryptionForInvitedMembers + } + + override fun getEncryptionAlgorithm(roomId: String): String? { + return cryptoStore.getRoomAlgorithm(roomId) + } + + /** + * Determine whether we should encrypt messages for invited users in this room. + * <p> + * Check here whether the invited members are allowed to read messages in the room history + * from the point they were invited onwards. + * + * @return true if we should encrypt messages for invited users. + */ + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return cryptoStore.shouldEncryptForInvitedMembers(roomId) + } + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param roomId the room identifier the event will be sent. + * @param callback the asynchronous callback + */ + override fun encryptEventContent(eventContent: Content, + eventType: String, + roomId: String, + callback: MatrixCallback<MXEncryptEventContentResult>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// if (!isStarted()) { +// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init") +// internalStart(false) +// } + val userIds = getRoomUserIds(roomId) + var alg = roomEncryptorsStore.get(roomId) + if (alg == null) { + val algorithm = getEncryptionAlgorithm(roomId) + if (algorithm != null) { + if (setEncryptionInRoom(roomId, algorithm, false, userIds)) { + alg = roomEncryptorsStore.get(roomId) + } + } + } + val safeAlgorithm = alg + if (safeAlgorithm != null) { + val t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | encryptEventContent() starts") + runCatching { + val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) + Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + MXEncryptEventContentResult(content, EventType.ENCRYPTED) + }.foldToCallback(callback) + } else { + val algorithm = getEncryptionAlgorithm(roomId) + val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, + algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) + Timber.e("## CRYPTO | encryptEventContent() : $reason") + callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) + } + } + } + + override fun discardOutboundSession(roomId: String) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + roomEncryptorsStore.get(roomId)?.discardSessionKey() + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or throw in case of error + */ + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + return internalDecryptEvent(event, timeline) + } + + /** + * Decrypt an event asynchronously + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param callback the callback to return data or null + */ + override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) { + cryptoCoroutineScope.launch { + val result = runCatching { + withContext(coroutineDispatchers.crypto) { + internalDecryptEvent(event, timeline) + } + } + result.foldToCallback(callback) + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or null in case of error + */ + @Throws(MXCryptoError::class) + private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val eventContent = event.content + if (eventContent == null) { + Timber.e("## CRYPTO | decryptEvent : empty event content") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } else { + val algorithm = eventContent["algorithm"]?.toString() + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) + if (alg == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) + Timber.e("## CRYPTO | decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } else { + try { + return alg.decryptEvent(event, timeline) + } catch (mxCryptoError: MXCryptoError) { + Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + if (algorithm == MXCRYPTO_ALGORITHM_OLM) { + if (mxCryptoError is MXCryptoError.Base + && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { + // need to find sending device + val olmContent = event.content.toModel<OlmEventContent>() + cryptoStore.getUserDevices(event.senderId ?: "") + ?.values + ?.firstOrNull { it.identityKey() == olmContent?.senderKey } + ?.let { + markOlmSessionForUnwedging(event.senderId ?: "", it) + } + ?: run { + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") + } + } + } + throw mxCryptoError + } + } + } + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timelineId the timeline id + */ + fun resetReplayAttackCheckInTimeline(timelineId: String) { + olmDevice.resetReplayAttackCheckInTimeline(timelineId) + } + + /** + * Handle the 'toDevice' event + * + * @param event the event + */ + fun onToDeviceEvent(event: Event) { + // event have already been decrypted + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { + cryptoStore.saveGossipingEvent(event) + // Keys are imported directly, not waiting for end of sync + onRoomKeyEvent(event) + } + EventType.REQUEST_SECRET, + EventType.ROOM_KEY_REQUEST -> { + // save audit trail + cryptoStore.saveGossipingEvent(event) + // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) + incomingGossipingRequestManager.onGossipingRequestEvent(event) + } + EventType.SEND_SECRET -> { + cryptoStore.saveGossipingEvent(event) + onSecretSendReceived(event) + } + EventType.ROOM_KEY_WITHHELD -> { + onKeyWithHeldReceived(event) + } + else -> { + // ignore + } + } + } + } + + /** + * Handle a key event. + * + * @param event the key event. + */ + private fun onRoomKeyEvent(event: Event) { + val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return + Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") + return + } + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) + if (alg == null) { + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + return + } + alg.onRoomKeyEvent(event, keysBackupService) + } + + private fun onKeyWithHeldReceived(event: Event) { + val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also { + Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + } + Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>") + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) + if (alg is IMXWithHeldExtension) { + alg.onRoomKeyWithHeldEvent(withHeldContent) + } else { + Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}") + return + } + } + + private fun onSecretSendReceived(event: Event) { + Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + if (!event.isEncrypted()) { + // secret send messages must be encrypted + Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (event.senderId != userId) { + Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + return + } + + val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return + + val existingRequest = cryptoStore + .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } + + if (existingRequest == null) { + Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + + if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") + } + } + + /** + * Returns true if handled by SDK, otherwise should be sent to application layer + */ + private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { + return when (secretName) { + MASTER_KEY_SSSS_NAME -> { + crossSigningService.onSecretMSKGossip(secretValue) + true + } + SELF_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretSSKGossip(secretValue) + true + } + USER_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretUSKGossip(secretValue) + true + } + KEYBACKUP_SECRET_SSSS_NAME -> { + keysBackupService.onSecretKeyGossip(secretValue) + true + } + else -> false + } + } + + /** + * Handle an m.room.encryption event. + * + * @param event the encryption event. + */ + private fun onRoomEncryptionEvent(roomId: String, event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val params = LoadRoomMembersTask.Params(roomId) + try { + loadRoomMembersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") + } finally { + val userIds = getRoomUserIds(roomId) + setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) + } + } + } + + private fun getRoomUserIds(roomId: String): List<String> { + var userIds: List<String> = emptyList() + monarchy.doWithRealm { realm -> + // Check whether the event content must be encrypted for the invited members. + val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() + && shouldEncryptForInvitedMembers(roomId) + + userIds = if (encryptForInvitedMembers) { + RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + } else { + RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() + } + } + return userIds + } + + /** + * Handle a change in the membership state of a member of a room. + * + * @param event the membership event causing the change + */ + private fun onRoomMembershipEvent(roomId: String, event: Event) { + roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return + + event.stateKey?.let { userId -> + val roomMember: RoomMemberSummary? = event.content.toModel() + val membership = roomMember?.membership + if (membership == Membership.JOIN) { + // make sure we are tracking the deviceList for this user. + deviceListManager.startTrackingDeviceList(listOf(userId)) + } else if (membership == Membership.INVITE + && shouldEncryptForInvitedMembers(roomId) + && isEncryptionEnabledForInvitedUser()) { + // track the deviceList for this invited user. + // Caution: there's a big edge case here in that federated servers do not + // know what other servers are in the room at the time they've been invited. + // They therefore will not send device updates if a user logs in whilst + // their state is invite. + deviceListManager.startTrackingDeviceList(listOf(userId)) + } + } + } + + private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + val eventContent = event.content.toModel<RoomHistoryVisibilityContent>() + eventContent?.historyVisibility?.let { + cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) + } + } + + /** + * Upload my user's device keys. + */ + private suspend fun uploadDeviceKeys() { + if (cryptoStore.getDeviceKeysUploaded()) { + Timber.d("Keys already uploaded, nothing to do") + return + } + // Prepare the device keys data to send + // Sign it + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) + var rest = getMyDevice().toRest() + + rest = rest.copy( + signatures = objectSigner.signObject(canonicalJson) + ) + + val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null) + uploadKeysTask.execute(uploadDeviceKeysParams) + + cryptoStore.setDeviceKeysUploaded(true) + } + + /** + * Export the crypto keys + * + * @param password the password + * @param callback the exported keys + */ + override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) + }.foldToCallback(callback) + } + } + + /** + * Export the crypto keys + * + * @param password the password + * @param anIterationCount the encryption iteration count (0 means no encryption) + */ + private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { + return withContext(coroutineDispatchers.crypto) { + val iterationCount = max(0, anIterationCount) + + val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } + + val adapter = MoshiProvider.providesMoshi() + .adapter(List::class.java) + + MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) + } + } + + /** + * Import the room keys + * + * @param roomKeysAsArray the room keys as array. + * @param password the password + * @param progressListener the progress listener + * @param callback the asynchronous callback. + */ + override fun importRoomKeys(roomKeysAsArray: ByteArray, + password: String, + progressListener: ProgressListener?, + callback: MatrixCallback<ImportRoomKeysResult>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + withContext(coroutineDispatchers.crypto) { + Timber.v("## CRYPTO | importRoomKeys starts") + + val t0 = System.currentTimeMillis() + val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) + val t1 = System.currentTimeMillis() + + Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") + + val importedSessions = MoshiProvider.providesMoshi() + .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) + .fromJson(roomKeys) + + val t2 = System.currentTimeMillis() + + Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") + + if (importedSessions == null) { + throw Exception("Error") + } + + megolmSessionDataImporter.handle(importedSessions, true, progressListener) + } + }.foldToCallback(callback) + } + } + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + override fun setWarnOnUnknownDevices(warn: Boolean) { + warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn) + } + + /** + * Check if the user ids list have some unknown devices. + * A success means there is no unknown devices. + * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered. + * + * @param userIds the user ids list + * @param callback the asynchronous callback. + */ + fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) { + // force the refresh to ensure that the devices list is up-to-date + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + val keys = deviceListManager.downloadKeys(userIds, true) + val unknownDevices = getUnknownDevices(keys) + if (unknownDevices.map.isNotEmpty()) { + // trigger an an unknown devices exception + throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)) + } + }.foldToCallback(callback) + } + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + cryptoStore.setGlobalBlacklistUnverifiedDevices(block) + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @return true to unilaterally blacklist all unverified devices. + */ + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return cryptoStore.getGlobalBlacklistUnverifiedDevices() + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * + * @param roomId the room id + * @return true if the client should encrypt messages only for the verified devices. + */ +// TODO add this info in CryptoRoomEntity? + override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { + return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) } + ?: false + } + + /** + * Manages the room black-listing for unverified devices. + * + * @param roomId the room id + * @param add true to add the room id to the list, false to remove it. + */ + private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { + val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() + + if (add) { + if (roomId !in roomIds) { + roomIds.add(roomId) + } + } else { + roomIds.remove(roomId) + } + + cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) + } + + /** + * Add this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + */ + override fun setRoomBlacklistUnverifiedDevices(roomId: String) { + setRoomBlacklistUnverifiedDevices(roomId, true) + } + + /** + * Remove this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + */ + override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { + setRoomBlacklistUnverifiedDevices(roomId, false) + } + +// TODO Check if this method is still necessary + /** + * Cancel any earlier room key request + * + * @param requestBody requestBody + */ + override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody) + } + + /** + * Re request the encryption keys required to decrypt an event. + * + * @param event the event to decrypt again. + */ + override fun reRequestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { + Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") + } + + val requestBody = RoomKeyRequestBody( + algorithm = wireContent.algorithm, + roomId = event.roomId, + senderKey = wireContent.senderKey, + sessionId = wireContent.sessionId + ) + + outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + } + + override fun requestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { + Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + } + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// if (!isStarted()) { +// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") +// internalStart(false) +// } + roomDecryptorProvider + .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) + ?.requestKeysForEvent(event, false) ?: run { + Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + } + } + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.addRoomKeysRequestListener(listener) + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) + } + + private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { + val deviceKey = deviceInfo.identityKey() + + val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 + val now = System.currentTimeMillis() + if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { + Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + return + } + + Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + lastNewSessionForcedDates.setObject(senderId, deviceKey, now) + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap<Any>() + sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + + /** + * Provides the list of unknown devices + * + * @param devicesInRoom the devices map + * @return the unknown devices map + */ + private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXUsersDevicesMap<CryptoDeviceInfo> { + val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>() + val userIds = devicesInRoom.userIds + for (userId in userIds) { + devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> + devicesInRoom.getObject(userId, deviceId) + ?.takeIf { it.isUnknown } + ?.let { + unknownDevices.setObject(userId, deviceId, it) + } + } + } + + return unknownDevices + } + + override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + deviceListManager.downloadKeys(userIds, forceDownload) + }.foldToCallback(callback) + } + } + + override fun addNewSessionListener(newSessionListener: NewSessionListener) { + roomDecryptorProvider.addNewSessionListener(newSessionListener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + roomDecryptorProvider.removeSessionListener(listener) + } +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString(): String { + return "DefaultCryptoService of $userId ($deviceId)" + } + + override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> { + return cryptoStore.getOutgoingRoomKeyRequests() + } + + override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { + return cryptoStore.getIncomingRoomKeyRequests() + } + + override fun getGossipingEventsTrail(): List<Event> { + return cryptoStore.getGossipingEventsTrail() + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) + } + /* ========================================================================================== + * For test only + * ========================================================================================== */ + + @VisibleForTesting + val cryptoStoreForTesting = cryptoStore + + companion object { + const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt new file mode 100755 index 0000000000000000000000000000000000000000..bb41edefe1813e1c10118244cd75850c2ae75562 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -0,0 +1,547 @@ +/* + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +// Legacy name: MXDeviceList +@SessionScope +internal class DeviceListManager @Inject constructor(private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val syncTokenStore: SyncTokenStore, + private val credentials: Credentials, + private val downloadKeysForUsersTask: DownloadKeysForUsersTask, + coroutineDispatchers: MatrixCoroutineDispatchers, + taskExecutor: TaskExecutor) { + + interface UserDevicesUpdateListener { + fun onUsersDeviceUpdate(userIds: List<String>) + } + + private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>() + + fun addListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.add(listener) + } + } + + fun removeListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.remove(listener) + } + } + + private fun dispatchDeviceChange(users: List<String>) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.forEach { + try { + it.onUsersDeviceUpdate(users) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to dispatch device change") + } + } + } + } + + // HS not ready for retry + private val notReadyToRetryHS = mutableSetOf<String>() + + init { + taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + for ((userId, status) in deviceTrackingStatuses) { + if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { + // if a download was in progress when we got shut down, it isn't any more. + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + } + + /** + * Tells if the key downloads should be tried + * + * @param userId the userId + * @return true if the keys download can be retrieved + */ + private fun canRetryKeysDownload(userId: String): Boolean { + var res = false + + if (':' in userId) { + try { + synchronized(notReadyToRetryHS) { + res = !notReadyToRetryHS.contains(userId.substringAfterLast(':')) + } + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") + } + } + + return res + } + + /** + * Clear the unavailable server lists + */ + private fun clearUnavailableServersList() { + synchronized(notReadyToRetryHS) { + notReadyToRetryHS.clear() + } + } + + /** + * Mark the cached device list for the given user outdated + * flag the given user for device-list tracking, if they are not already. + * + * @param userIds the user ids list + */ + fun startTrackingDeviceList(userIds: List<String>?) { + if (null != userIds) { + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in userIds) { + if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { + Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + } + + /** + * Update the devices list statuses + * + * @param changed the user ids list which have new devices + * @param left the user ids list which left a room + */ + fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) { + Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left") + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in changed) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + + for (userId in left) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + + /** + * This will flag each user whose devices we are tracking as in need of an + * + update + */ + fun invalidateAllDeviceLists() { + handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList()) + } + + /** + * The keys download failed + * + * @param userIds the user ids list + */ + private fun onKeysDownloadFailed(userIds: List<String>) { + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD } + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + + /** + * The keys download succeeded. + * + * @param userIds the userIds list + * @param failures the failure map. + */ + private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> { + if (failures != null) { + for ((k, value) in failures) { + val statusCode = when (val status = value["status"]) { + is Double -> status.toInt() + is Int -> status.toInt() + else -> 0 + } + if (statusCode == 503) { + synchronized(notReadyToRetryHS) { + notReadyToRetryHS.add(k) + } + } + } + } + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>() + for (userId in userIds) { + val devices = cryptoStore.getUserDevices(userId) + if (null == devices) { + if (canRetryKeysDownload(userId)) { + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + Timber.e("failed to retry the devices of $userId : retry later") + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER + Timber.e("failed to retry the devices of $userId : the HS is not available") + } + } + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + // we didn't get any new invalidations since this download started: + // this user's device list is now up to date. + deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE + Timber.v("Device list for $userId now up to date") + } + // And the response result + usersDevicesInfoMap.setObjects(userId, devices) + } + } + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + + dispatchDeviceChange(userIds) + return usersDevicesInfoMap + } + + /** + * Download the device keys for a list of users and stores the keys in the MXStore. + * It must be called in getEncryptingThreadHandler() thread. + * + * @param userIds The users to fetch. + * @param forceDownload Always download the keys even if cached. + */ + suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> { + Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") + // Map from userId -> deviceId -> MXDeviceInfo + val stored = MXUsersDevicesMap<CryptoDeviceInfo>() + + // List of user ids we need to download keys for + val downloadUsers = ArrayList<String>() + if (null != userIds) { + if (forceDownload) { + downloadUsers.addAll(userIds) + } else { + for (userId in userIds) { + val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED) + // downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys + // not yet retrieved + if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) { + downloadUsers.add(userId) + } else { + val devices = cryptoStore.getUserDevices(userId) + // should always be true + if (devices != null) { + stored.setObjects(userId, devices) + } else { + downloadUsers.add(userId) + } + } + } + } + } + return if (downloadUsers.isEmpty()) { + Timber.v("## CRYPTO | downloadKeys() : no new user device") + stored + } else { + Timber.v("## CRYPTO | downloadKeys() : starts") + val t0 = System.currentTimeMillis() + val result = doKeyDownloadForUsers(downloadUsers) + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") + result.also { + it.addEntriesFromMap(stored) + } + } + } + + /** + * Download the devices keys for a set of users. + * + * @param downloadUsers the user ids list + */ + private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> { + Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") + // get the user ids which did not already trigger a keys download + val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } + if (filteredUsers.isEmpty()) { + // trigger nothing + return MXUsersDevicesMap() + } + val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) + val response = try { + downloadKeysForUsersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") + onKeysDownloadFailed(filteredUsers) + throw throwable + } + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + for (userId in filteredUsers) { + // al devices = + val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } + + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") + if (!models.isNullOrEmpty()) { + val workingCopy = models.toMutableMap() + for ((deviceId, deviceInfo) in models) { + // Get the potential previously store device keys for this device + val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId) + + // in some race conditions (like unit tests) + // the self device must be seen as verified + if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { + deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true) + } + // Validate received keys + if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { + // New device keys are not valid. Do not store them + workingCopy.remove(deviceId) + if (null != previouslyStoredDeviceKeys) { + // But keep old validated ones if any + workingCopy[deviceId] = previouslyStoredDeviceKeys + } + } else if (null != previouslyStoredDeviceKeys) { + // The verified status is not sync'ed with hs. + // This is a client side information, valid only for this client. + // So, transfer its previous value + workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel + } + } + // Update the store + // Note that devices which aren't in the response will be removed from the stores + cryptoStore.storeUserDevices(userId, workingCopy) + } + + // Handle cross signing keys update + val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + } + val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + } + val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + } + cryptoStore.storeUserCrossSigningKeys( + userId, + masterKey, + selfSigningKey, + userSigningKey + ) + } + + // Update devices trust for these users + dispatchDeviceChange(downloadUsers) + + return onKeysDownloadSucceed(filteredUsers, response.failures) + } + + /** + * Validate device keys. + * This method must called on getEncryptingThreadHandler() thread. + * + * @param deviceKeys the device keys to validate. + * @param userId the id of the user of the device. + * @param deviceId the id of the device. + * @param previouslyStoredDeviceKeys the device keys we received before for this device + * @return true if succeeds + */ + private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { + if (null == deviceKeys) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.keys) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.signatures) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") + return false + } + + // Check that the user_id and device_id in the received deviceKeys are correct + if (deviceKeys.userId != userId) { + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") + return false + } + + if (deviceKeys.deviceId != deviceId) { + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") + return false + } + + val signKeyId = "ed25519:" + deviceKeys.deviceId + val signKey = deviceKeys.keys[signKeyId] + + if (null == signKey) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") + return false + } + + val signatureMap = deviceKeys.signatures[userId] + + if (null == signatureMap) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") + return false + } + + val signature = signatureMap[signKeyId] + + if (null == signature) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") + return false + } + + var isVerified = false + var errorMessage: String? = null + + try { + olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + if (!isVerified) { + Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage) + return false + } + + if (null != previouslyStoredDeviceKeys) { + if (previouslyStoredDeviceKeys.fingerprint() != signKey) { + // This should only happen if the list has been MITMed; we are + // best off sticking with the original keys. + // + // Should we warn the user about it somehow? + Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + + Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") + Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") + + return false + } + } + + return true + } + + /** + * Start device queries for any users who sent us an m.new_device recently + * This method must be called on getEncryptingThreadHandler() thread. + */ + suspend fun refreshOutdatedDeviceLists() { + Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> + TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId] + } + + if (users.isEmpty()) { + return + } + + // update the statuses + users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS } + + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + runCatching { + doKeyDownloadForUsers(users) + }.fold( + { + Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") + }, + { + Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + } + ) + } + + companion object { + + /** + * State transition diagram for DeviceList.deviceTrackingStatus + * <pre> + * + * | + * stopTrackingDeviceList V + * +---------------------> NOT_TRACKED + * | | + * +<--------------------+ | startTrackingDeviceList + * | | V + * | +-------------> PENDING_DOWNLOAD <--------------------+-+ + * | | ^ | | | + * | | restart download | | start download | | invalidateUserDeviceList + * | | client failed | | | | + * | | | V | | + * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ | + * | | | | + * +<-------------------+ | download successful | + * ^ V | + * +----------------------- UP_TO_DATE ------------------------+ + * + * </pre> + */ + + const val TRACKING_STATUS_NOT_TRACKED = -1 + const val TRACKING_STATUS_PENDING_DOWNLOAD = 1 + const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2 + const val TRACKING_STATUS_UP_TO_DATE = 3 + const val TRACKING_STATUS_UNREACHABLE_SERVER = 4 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c2e498863bb0ff73870d8b37ec78947630bb65b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +enum class GossipRequestType { + KEY, + SECRET +} + +enum class GossipingRequestState { + NONE, + PENDING, + REJECTED, + ACCEPTING, + ACCEPTED, + FAILED_TO_ACCEPTED, + // USER_REJECTED, + UNABLE_TO_PROCESS, + CANCELLED_BY_REQUESTER, + RE_REQUESTED +} + +enum class OutgoingGossipingRequestState { + UNSENT, + SENDING, + SENT, + CANCELLING, + CANCELLED, + FAILED_TO_SEND, + FAILED_TO_CANCEL +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..50e11e40b85df243682e0fccbbe0ba39c6cf3c82 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.startChain +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@SessionScope +internal class GossipingWorkManager @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { + + inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) + .build() + } + + // Prevent sending queue to stay broken after app restart + // The unique queue id will stay the same as long as this object is instanciated + val queueSuffixApp = System.currentTimeMillis() + + fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager + .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest) + .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..da7218613602855f859c5f77280ed7609c6635d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -0,0 +1,434 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +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 +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class IncomingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoConfig: MXCryptoConfig, + private val gossipingWorkManager: GossipingWorkManager, + private val roomEncryptorsStore: RoomEncryptorsStore, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope) { + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>() + private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>() + + // the listeners + private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet() + + init { + receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) + } + + // Recently verified devices (map of deviceId and timestamp) + private val recentlyVerifiedDevices = HashMap<String, Long>() + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfil gossiping requests + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + synchronized(recentlyVerifiedDevices) { + recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() + } + } + + private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp: Long? + synchronized(recentlyVerifiedDevices) { + verifTimestamp = recentlyVerifiedDevices[deviceId] + } + if (verifTimestamp == null) return false + + val age = System.currentTimeMillis() - verifTimestamp + + return age < FIVE_MINUTES_IN_MILLIS + } + + /** + * Called when we get an m.room_key_request event + * It must be called on CryptoThread + * + * @param event the announcement event. + */ + fun onGossipingRequestEvent(event: Event) { + Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") + val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>() + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + when (roomKeyShare?.action) { + GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { + if (event.getClearType() == EventType.REQUEST_SECRET) { + IncomingSecretShareRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + // save in DB + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { + IncomingRoomKeyRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } + } + GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { + IncomingRequestCancellation.fromEvent(event)?.let { + receivedRequestCancellations.add(it) + } + } + else -> { + Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") + } + } + } + + /** + * Process any m.room_key_request or m.secret.request events which were queued up during the + * current sync. + * It must be called on CryptoThread + */ + fun processReceivedGossipingRequests() { + val roomKeyRequestsToProcess = receivedGossipingRequests.toList() + receivedGossipingRequests.clear() + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } + } + + var receivedRequestCancellations: List<IncomingRequestCancellation>? = null + + synchronized(this.receivedRequestCancellations) { + if (this.receivedRequestCancellations.isNotEmpty()) { + receivedRequestCancellations = this.receivedRequestCancellations.toList() + this.receivedRequestCancellations.clear() + } + } + + receivedRequestCancellations?.forEach { request -> + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { + // ignore remote echo + return@forEach + } + val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") + if (matchingIncoming == null) { + // ignore that? + return@forEach + } else { + // If it was accepted from this device, keep the information, do not mark as cancelled + if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { + onRoomKeyRequestCancellation(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } + } + } + } + + private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { + val userId = request.userId ?: return + val deviceId = request.deviceId ?: return + val body = request.requestBody ?: return + val roomId = body.roomId ?: return + val alg = body.algorithm ?: return + + Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") + if (credentials.userId != userId) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user") + val senderKey = body.senderKey ?: return Unit + .also { Timber.w("missing senderKey") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + val sessionId = body.sessionId ?: return Unit + .also { Timber.w("missing sessionId") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + + if (alg != MXCRYPTO_ALGORITHM_MEGOLM) { + return Unit + .also { Timber.w("Only megolm is accepted here") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + } + + val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit + .also { Timber.w("no room Encryptor") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) + + if (isSuccess) { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } else { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) + } + } + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED) + return + } + // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) + if (null == decryptor) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + if (!decryptor.hasKeysForKeyRequest(request)) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + if (credentials.deviceId == deviceId && credentials.userId == userId) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + request.share = Runnable { + decryptor.shareKeysWithDevice(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + // if the device is verified already, share the keys + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device != null) { + if (device.isVerified) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") + request.share?.run() + return + } + + if (device.isBlocked) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + } + + // As per config we automatically discard untrusted devices request + if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { + Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") + // At this point the device is unknown, we don't want to bother user with that + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + // Pass to application layer to decide what to do + onRoomKeyRequest(request) + } + + private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { + val secretName = request.secretName ?: return Unit.also { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name") + } + + val userId = request.userId + if (userId == null || credentials.userId != userId) { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val deviceId = request.deviceId + ?: return Unit.also { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + val device = cryptoStore.getUserDevice(userId, deviceId) + ?: return Unit.also { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + if (!device.isVerified || device.isBlocked) { + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() + + when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + }?.let { secretValue -> + Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { + val params = SendGossipWorker.Params( + sessionId = sessionId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } else { + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + return + } + + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") + + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + request.share = { secretValue -> + + val params = SendGossipWorker.Params( + sessionId = userId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + + onShareRequest(request) + } + + /** + * Dispatch onRoomKeyRequest + * + * @param request the request + */ + private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequest(request) + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed") + } + } + } + } + + /** + * Ask for a value to the listeners, and take the first one + */ + private fun onShareRequest(request: IncomingSecretShareRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + if (listener.onSecretShareRequest(request)) { + return + } + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed") + } + } + } + // Not handled, ignore + request.ignore?.run() + } + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequestCancellation(request) + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed") + } + } + } + } + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + companion object { + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt new file mode 100755 index 0000000000000000000000000000000000000000..04b78fc89f6a740ab1df2e83e565b8d8db346017 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation + +/** + * IncomingRequestCancellation describes the incoming room key cancellation. + */ +data class IncomingRequestCancellation( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRequestCancellation? { + return event.getClearContent() + .toModel<ShareRequestCancellation>() + ?.let { + IncomingRequestCancellation( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt new file mode 100755 index 0000000000000000000000000000000000000000..04e18bf7f964f4e4df5863acc763e1722b15edf6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingRoomKeyRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val requestBody: RoomKeyRequestBody? = null, + + val state: GossipingRequestState = GossipingRequestState.NONE, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: Runnable? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRoomKeyRequest? { + return event.getClearContent() + .toModel<RoomKeyShareRequest>() + ?.let { + IncomingRoomKeyRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + requestBody = it.body ?: RoomKeyRequestBody(), + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt new file mode 100755 index 0000000000000000000000000000000000000000..4b91ed5d76bf892e61d7c27e2c67ff571d7e5a40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingSecretShareRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val secretName: String? = null, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: ((String) -> Unit)? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + + override val localCreationTimestamp: Long? + +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingSecretShareRequest? { + return event.getClearContent() + .toModel<SecretShareRequest>() + ?.let { + IncomingSecretShareRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + secretName = it.secretName, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt new file mode 100644 index 0000000000000000000000000000000000000000..d57584f49fe01a21e94572d983f7cfaf5098f4e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +interface IncomingShareRequestCommon { + /** + * The user id + */ + val userId: String? + + /** + * The device id + */ + val deviceId: String? + + /** + * The request id + */ + val requestId: String? + + val localCreationTimestamp: Long? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt new file mode 100755 index 0000000000000000000000000000000000000000..90b0b318b95dfa61cd2bf92153c0d3ad080b92b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +// TODO Update comment +internal object MXCryptoAlgorithms { + + /** + * Get the class implementing encryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXEncrypting'. + */ + fun hasEncryptorClassForAlgorithm(algorithm: String?): Boolean { + return when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM, + MXCRYPTO_ALGORITHM_OLM -> true + else -> false + } + } + + /** + * Get the class implementing decryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXDecrypting'. + */ + + fun hasDecryptorClassForAlgorithm(algorithm: String?): Boolean { + return when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM, + MXCRYPTO_ALGORITHM_OLM -> true + else -> false + } + } + + /** + * @return The list of registered algorithms. + */ + fun supportedAlgorithms(): List<String> { + return listOf(MXCRYPTO_ALGORITHM_MEGOLM, MXCRYPTO_ALGORITHM_OLM) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt new file mode 100755 index 0000000000000000000000000000000000000000..f094b5c6564373fcaa8b81800dcc02dabef455da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.util.JsonDict + +/** + * The result of a (successful) call to decryptEvent. + */ +data class MXEventDecryptionResult( + + /** + * The plaintext payload for the event (typically containing "type" and "content" fields). + */ + val clearEvent: JsonDict, + + /** + * Key owned by the sender of this event. + * See MXEvent.senderKey. + */ + val senderCurve25519Key: String? = null, + + /** + * Ed25519 key claimed by the sender of this event. + * See MXEvent.claimedEd25519Key. + */ + val claimedEd25519Key: String? = null, + + /** + * List of curve25519 keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. + */ + val forwardingCurve25519KeyChain: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt new file mode 100755 index 0000000000000000000000000000000000000000..4526ba8a51d0dd3bbe10f75289d55dcc841b54c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2017 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Base64 +import org.matrix.android.sdk.internal.extensions.toUnsignedInt +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.and +import kotlin.experimental.xor +import kotlin.math.min + +/** + * Utility class to import/export the crypto data + */ +object MXMegolmExportEncryption { + private const val HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----" + private const val TRAILER_LINE = "-----END MEGOLM SESSION DATA-----" + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + private const val LINE_LENGTH = 72 * 4 / 3 + + // default iteration count to export the e2e keys + const val DEFAULT_ITERATION_COUNT = 500000 + + /** + * Extract the AES key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the AES key + */ + private fun getAesKey(keyBits: ByteArray): ByteArray { + return keyBits.copyOfRange(0, 32) + } + + /** + * Extract the Hmac key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the Hmac key. + */ + private fun getHmacKey(keyBits: ByteArray): ByteArray { + return keyBits.copyOfRange(32, keyBits.size) + } + + /** + * Decrypt a megolm key file + * + * @param data the data to decrypt + * @param password the password. + * @return the decrypted output. + * @throws Exception the failure reason + */ + @Throws(Exception::class) + fun decryptMegolmKeyFile(data: ByteArray, password: String): String { + val body = unpackMegolmKeyFile(data) + + // check we have a version byte + if (null == body || body.isEmpty()) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Invalid file: too short") + } + + val version = body[0] + if (version.toInt() != 1) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Unsupported version") + } + + val ciphertextLength = body.size - (1 + 16 + 16 + 4 + 32) + if (ciphertextLength < 0) { + throw Exception("Invalid file: too short") + } + + if (password.isEmpty()) { + throw Exception("Empty password is not supported") + } + + val salt = body.copyOfRange(1, 1 + 16) + val iv = body.copyOfRange(17, 17 + 16) + val iterations = + (body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt() + val ciphertext = body.copyOfRange(37, 37 + ciphertextLength) + val hmac = body.copyOfRange(body.size - 32, body.size) + + val deriveKey = deriveKeys(salt, iterations, password) + + val toVerify = body.copyOfRange(0, body.size - 32) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toVerify) + + if (!hmac.contentEquals(digest)) { + Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?") + throw Exception("Authentication check failed: incorrect password?") + } + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(ciphertext)) + outStream.write(decryptCipher.doFinal()) + + val decodedString = String(outStream.toByteArray(), Charset.defaultCharset()) + outStream.close() + + return decodedString + } + + /** + * Encrypt a string into the megolm export format. + * + * @param data the data to encrypt. + * @param password the password + * @param kdf_rounds the iteration count + * @return the encrypted data + * @throws Exception the failure reason + */ + @Throws(Exception::class) + @JvmOverloads + fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { + if (password.isEmpty()) { + throw Exception("Empty password is not supported") + } + + val secureRandom = SecureRandom() + + val salt = ByteArray(16) + secureRandom.nextBytes(salt) + + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val deriveKey = deriveKeys(salt, kdf_rounds, password) + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(data.toByteArray(charset("UTF-8")))) + outStream.write(decryptCipher.doFinal()) + + val cipherArray = outStream.toByteArray() + val bodyLength = 1 + salt.size + iv.size + 4 + cipherArray.size + 32 + + val resultBuffer = ByteArray(bodyLength) + var idx = 0 + resultBuffer[idx++] = 1 // version + + System.arraycopy(salt, 0, resultBuffer, idx, salt.size) + idx += salt.size + + System.arraycopy(iv, 0, resultBuffer, idx, iv.size) + idx += iv.size + + resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds and 0xff).toByte() + + System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) + idx += cipherArray.size + + val toSign = resultBuffer.copyOfRange(0, idx) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toSign) + System.arraycopy(digest, 0, resultBuffer, idx, digest.size) + + return packMegolmKeyFile(resultBuffer) + } + + /** + * Unbase64 an ascii-armoured megolm key file + * Strips the header and trailer lines, and unbase64s the content + * + * @param data the input data + * @return unbase64ed content + */ + @Throws(Exception::class) + private fun unpackMegolmKeyFile(data: ByteArray): ByteArray? { + val fileStr = String(data, Charset.defaultCharset()) + + // look for the start line + var lineStart = 0 + + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Header line not found") + throw Exception("Header line not found") + } + + val line = fileStr.substring(lineStart, lineEnd).trim() + + // start the next line after the newline + lineStart = lineEnd + 1 + + if (line == HEADER_LINE) { + break + } + } + + val dataStart = lineStart + + // look for the end line + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + val line = if (lineEnd < 0) { + fileStr.substring(lineStart) + } else { + fileStr.substring(lineStart, lineEnd) + }.trim() + + if (line == TRAILER_LINE) { + break + } + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Trailer line not found") + throw Exception("Trailer line not found") + } + + // start the next line after the newline + lineStart = lineEnd + 1 + } + + val dataEnd = lineStart + + // Receiving side + return Base64.decode(fileStr.substring(dataStart, dataEnd), Base64.DEFAULT) + } + + /** + * Pack the megolm data. + * + * @param data the data to pack. + * @return the packed data + * @throws Exception the failure reason. + */ + @Throws(Exception::class) + private fun packMegolmKeyFile(data: ByteArray): ByteArray { + val nLines = (data.size + LINE_LENGTH - 1) / LINE_LENGTH + + val outStream = ByteArrayOutputStream() + outStream.write(HEADER_LINE.toByteArray()) + + var o = 0 + + for (i in 1..nLines) { + outStream.write("\n".toByteArray()) + + val len = min(LINE_LENGTH, data.size - o) + outStream.write(Base64.encode(data, o, len, Base64.DEFAULT)) + o += LINE_LENGTH + } + + outStream.write("\n".toByteArray()) + outStream.write(TRAILER_LINE.toByteArray()) + outStream.write("\n".toByteArray()) + + return outStream.toByteArray() + } + + /** + * Derive the AES and HMAC-SHA-256 keys for the file + * + * @param salt salt for pbkdf + * @param iterations number of pbkdf iterations + * @param password password + * @return the derived keys + */ + @Throws(Exception::class) + private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + val prf = Mac.getInstance("HmacSHA512") + prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) + + // 512 bits key length + val key = ByteArray(64) + val Uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) + prf.update(salt) + val int32BE = ByteArray(4) { 0.toByte() } + int32BE[3] = 1.toByte() + prf.update(int32BE) + prf.doFinal(Uc, 0) + + // copy to the key + System.arraycopy(Uc, 0, key, 0, Uc.size) + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(Uc) + prf.doFinal(Uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in Uc.indices) { + key[byteIndex] = key[byteIndex] xor Uc[byteIndex] + } + } + + Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms") + + return key + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt new file mode 100755 index 0000000000000000000000000000000000000000..cfdd050801265ee12123e7eeb95e7c94d2e5bd7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -0,0 +1,779 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertFromUTF8 +import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import org.matrix.olm.OlmMessage +import org.matrix.olm.OlmOutboundGroupSession +import org.matrix.olm.OlmSession +import org.matrix.olm.OlmUtility +import timber.log.Timber +import java.net.URLEncoder +import javax.inject.Inject + +// The libolm wrapper. +@SessionScope +internal class MXOlmDevice @Inject constructor( + /** + * The store where crypto data is saved. + */ + private val store: IMXCryptoStore) { + + /** + * @return the Curve25519 key for the account. + */ + var deviceCurve25519Key: String? = null + private set + + /** + * @return the Ed25519 key for the account. + */ + var deviceEd25519Key: String? = null + private set + + // The OLM lib utility instance. + private var olmUtility: OlmUtility? = null + + // The outbound group session. + // They are not stored in 'store' to avoid to remember to which devices we sent the session key. + // Plus, in cryptography, it is good to refresh sessions from time to time. + // The key is the session id, the value the outbound group session. + private val outboundGroupSessionStore: MutableMap<String, OlmOutboundGroupSession> = HashMap() + + // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // The Matrix SDK exposes events through MXEventTimelines. A developer can open several + // timelines from a same room so that a message can be decrypted several times but from + // a different timeline. + // So, store these message indexes per timeline id. + // + // The first level keys are timeline ids. + // The second level keys are strings of form "<senderKey>|<session_id>|<message_index>" + private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap() + + init { + // Retrieve the account from the store + try { + store.getOrCreateOlmAccount() + } catch (e: Exception) { + Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") + } + + try { + olmUtility = OlmUtility() + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : OlmUtility failed with error") + olmUtility = null + } + + try { + deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") + } + + try { + deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") + } + } + + /** + * @return The current (unused, unpublished) one-time keys for this account. + */ + fun getOneTimeKeys(): Map<String, Map<String, String>>? { + try { + return store.getOlmAccount().oneTimeKeys() + } catch (e: Exception) { + Timber.e(e, "## getOneTimeKeys() : failed") + } + + return null + } + + /** + * @return The maximum number of one-time keys the olm account can store. + */ + fun getMaxNumberOfOneTimeKeys(): Long { + return store.getOlmAccount().maxOneTimeKeys() + } + + /** + * Release the instance + */ + fun release() { + olmUtility?.releaseUtility() + } + + /** + * Signs a message with the ed25519 key for this account. + * + * @param message the message to be signed. + * @return the base64-encoded signature. + */ + fun signMessage(message: String): String? { + try { + return store.getOlmAccount().signMessage(message) + } catch (e: Exception) { + Timber.e(e, "## signMessage() : failed") + } + + return null + } + + /** + * Marks all of the one-time keys as published. + */ + fun markKeysAsPublished() { + try { + store.getOlmAccount().markOneTimeKeysAsPublished() + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## markKeysAsPublished() : failed") + } + } + + /** + * Generate some new one-time keys + * + * @param numKeys number of keys to generate + */ + fun generateOneTimeKeys(numKeys: Int) { + try { + store.getOlmAccount().generateOneTimeKeys(numKeys) + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## generateOneTimeKeys() : failed") + } + } + + /** + * Generate a new outbound session. + * The new session will be stored in the MXStore. + * + * @param theirIdentityKey the remote user's Curve25519 identity key + * @param theirOneTimeKey the remote user's one-time Curve25519 key + * @return the session id for the outbound session. + */ + fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { + Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") + var olmSession: OlmSession? = null + + try { + olmSession = OlmSession() + olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey) + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + olmSessionWrapper.onMessageReceived() + + store.storeSession(olmSessionWrapper, theirIdentityKey) + + val sessionIdentifier = olmSession.sessionIdentifier() + + Timber.v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") + return sessionIdentifier + } catch (e: Exception) { + Timber.e(e, "## createOutboundSession() failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Generate a new inbound session, given an incoming message. + * + * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. + * @param messageType the message_type field from the received message (must be 0). + * @param ciphertext base64-encoded body from the received message. + * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. + */ + fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? { + Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") + + var olmSession: OlmSession? = null + + try { + try { + olmSession = OlmSession() + olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : the session creation failed") + return null + } + + Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") + + try { + store.getOlmAccount().removeOneTimeKeys(olmSession) + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") + } + + Timber.v("## createInboundSession() : ciphertext: $ciphertext") + try { + val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8")) + Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256") + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") + } + + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + var payloadString: String? = null + + try { + payloadString = olmSession.decryptMessage(olmMessage) + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + // This counts as a received message: set last received message time to now + olmSessionWrapper.onMessageReceived() + + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : decryptMessage failed") + } + + val res = HashMap<String, String>() + + if (!payloadString.isNullOrEmpty()) { + res["payload"] = payloadString + } + + val sessionIdentifier = olmSession.sessionIdentifier() + + if (!sessionIdentifier.isNullOrEmpty()) { + res["session_id"] = sessionIdentifier + } + + return res + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : OlmSession creation failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Get a list of known session IDs for the given device. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return a list of known session ids for the device. + */ + fun getSessionIds(theirDeviceIdentityKey: String): Set<String>? { + return store.getDeviceSessionIds(theirDeviceIdentityKey) + } + + /** + * Get the right olm session id for encrypting messages to the given identity key. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return the session id, or null if no established session. + */ + fun getSessionId(theirDeviceIdentityKey: String): String? { + return store.getLastUsedSessionId(theirDeviceIdentityKey) + } + + /** + * Encrypt an outgoing message using an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session + * @param payloadString the payload to be encrypted and sent + * @return the cipher text + */ + fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? { + var res: MutableMap<String, Any>? = null + val olmMessage: OlmMessage + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (olmSessionWrapper != null) { + try { + Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") + // Timber.v("## encryptMessage() : payloadString: " + payloadString); + + olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString) + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + res = HashMap() + + res["body"] = olmMessage.mCipherText + res["type"] = olmMessage.mType + } catch (e: Exception) { + Timber.e(e, "## encryptMessage() : failed") + } + } else { + Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId") + } + + return res + } + + /** + * Decrypt an incoming message using an existing session. + * + * @param ciphertext the base64-encoded body from the received message. + * @param messageType message_type field from the received message. + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @return the decrypted payload. + */ + fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { + var payloadString: String? = null + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (null != olmSessionWrapper) { + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + try { + payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage) + olmSessionWrapper.onMessageReceived() + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## decryptMessage() : decryptMessage failed") + } + } + + return payloadString + } + + /** + * Determine if an incoming messages is a prekey message matching an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. + * @return YES if the received message is a prekey message which matchesthe given session. + */ + fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { + if (messageType != 0) { + return false + } + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) + } + + // Outbound group session + + /** + * Generate a new outbound group session. + * + * @return the session id for the outbound session. + */ + fun createOutboundGroupSession(): String? { + var session: OlmOutboundGroupSession? = null + try { + session = OlmOutboundGroupSession() + outboundGroupSessionStore[session.sessionIdentifier()] = session + return session.sessionIdentifier() + } catch (e: Exception) { + Timber.e(e, "createOutboundGroupSession") + + session?.releaseSession() + } + + return null + } + + /** + * Get the current session key of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the base64-encoded secret key. + */ + fun getSessionKey(sessionId: String): String? { + if (sessionId.isNotEmpty()) { + try { + return outboundGroupSessionStore[sessionId]!!.sessionKey() + } catch (e: Exception) { + Timber.e(e, "## getSessionKey() : failed") + } + } + return null + } + + /** + * Get the current message index of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the current chain index. + */ + fun getMessageIndex(sessionId: String): Int { + return if (sessionId.isNotEmpty()) { + outboundGroupSessionStore[sessionId]!!.messageIndex() + } else 0 + } + + /** + * Encrypt an outgoing message with an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @param payloadString the payload to be encrypted and sent. + * @return ciphertext + */ + fun encryptGroupMessage(sessionId: String, payloadString: String): String? { + if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { + try { + return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) + } catch (e: Exception) { + Timber.e(e, "## encryptGroupMessage() : failed") + } + } + return null + } + + // Inbound group session + + /** + * Add an inbound group session to the session store. + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + * @param roomId the id of the room in which this session will be used. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. + * @param keysClaimed Other keys the sender claims. + * @param exportFormat true if the megolm keys are in export format + * @return true if the operation succeeds. + */ + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List<String>, + keysClaimed: Map<String, String>, + exportFormat: Boolean): Boolean { + val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // If we already have this session, consider updating it + Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + val existingFirstKnown = it.firstKnownIndex!! + val newKnownFirstIndex = session.firstKnownIndex + + // If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + session.olmInboundGroupSession?.releaseSession() + return false + } + }, + { + // Nothing to do in case of error + } + ) + + // sanity check + if (null == session.olmInboundGroupSession) { + Timber.e("## addInboundGroupSession : invalid session") + return false + } + + try { + if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) { + Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + session.olmInboundGroupSession!!.releaseSession() + return false + } + } catch (e: Exception) { + session.olmInboundGroupSession?.releaseSession() + Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed") + return false + } + + session.senderKey = senderKey + session.roomId = roomId + session.keysClaimed = keysClaimed + session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + + store.storeInboundGroupSessions(listOf(session)) + + return true + } + + /** + * Import an inbound group sessions to the session store. + * + * @param megolmSessionsData the megolm sessions data + * @return the successfully imported sessions. + */ + fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> { + val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size) + + for (megolmSessionData in megolmSessionsData) { + val sessionId = megolmSessionData.sessionId + val senderKey = megolmSessionData.senderKey + val roomId = megolmSessionData.roomId + + var session: OlmInboundGroupSessionWrapper2? = null + + try { + session = OlmInboundGroupSessionWrapper2(megolmSessionData) + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + } + + // sanity check + if (session?.olmInboundGroupSession == null) { + Timber.e("## importInboundGroupSession : invalid session") + continue + } + + try { + if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) { + Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession() + continue + } + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed") + session.olmInboundGroupSession!!.releaseSession() + continue + } + + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // If we already have this session, consider updating it + Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + // For now we just ignore updates. TODO: implement something here + if (it.firstKnownIndex!! <= session.firstKnownIndex!!) { + // Ignore this, keep existing + session.olmInboundGroupSession!!.releaseSession() + } else { + sessions.add(session) + } + Unit + }, + { + // Session does not already exist, add it + sessions.add(session) + } + + ) + } + + store.storeInboundGroupSessions(sessions) + + return sessions + } + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + */ + fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) { + if (null != sessionId && null != sessionKey) { + store.removeInboundGroupSession(sessionId, sessionKey) + } + } + + /** + * Decrypt a received message with an inbound group session. + * + * @param body the base64-encoded body of the encrypted message. + * @param roomId the room in which the message was received. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the decrypting result. Nil if the sessionId is unknown. + */ + @Throws(MXCryptoError::class) + fun decryptGroupMessage(body: String, + roomId: String, + timeline: String?, + sessionId: String, + senderKey: String): OlmDecryptionResult { + val session = getInboundGroupSession(sessionId, senderKey, roomId) + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId == session.roomId) { + val decryptResult = try { + session.olmInboundGroupSession!!.decryptMessage(body) + } catch (e: OlmException) { + Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") + throw MXCryptoError.OlmError(e) + } + + if (timeline?.isNotBlank() == true) { + val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } + + val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex + + if (timelineSet.contains(messageIndexKey)) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) + } + + timelineSet.add(messageIndexKey) + } + + store.storeInboundGroupSessions(listOf(session)) + val payload = try { + val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) + val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) + adapter.fromJson(payloadString) + } catch (e: Exception) { + Timber.e("## decryptGroupMessage() : fails to parse the payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + return OlmDecryptionResult( + payload, + session.keysClaimed, + senderKey, + session.forwardingCurve25519KeyChain + ) + } else { + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) + } + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timeline the id of the timeline. + */ + fun resetReplayAttackCheckInTimeline(timeline: String?) { + if (null != timeline) { + inboundGroupSessionMessageIndexes.remove(timeline) + } + } + +// Utilities + + /** + * Verify an ed25519 signature on a JSON object. + * + * @param key the ed25519 key. + * @param jsonDictionary the JSON object which was signed. + * @param signature the base64-encoded signature to be checked. + * @throws Exception the exception + */ + @Throws(Exception::class) + fun verifySignature(key: String, jsonDictionary: Map<String, Any>, signature: String) { + // Check signature on the canonical version of the JSON + olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) + } + + /** + * Calculate the SHA-256 hash of the input and encodes it as base64. + * + * @param message the message to hash. + * @return the base64-encoded hash value. + */ + fun sha256(message: String): String { + return olmUtility!!.sha256(convertToUTF8(message)) + } + + /** + * Search an OlmSession + * + * @param theirDeviceIdentityKey the device key + * @param sessionId the session Id + * @return the olm session + */ + private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { + // sanity check + return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { + store.getDeviceSession(sessionId, theirDeviceIdentityKey) + } + } + + /** + * Extract an InboundGroupSession from the session store and do some check. + * inboundGroupSessionWithIdError describes the failure reason. + * + * @param roomId the room where the session is used. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the inbound group session. + */ + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 { + if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) + } + + val session = store.getInboundGroupSession(sessionId, senderKey) + + if (session != null) { + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId != session.roomId) { + val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.e("## getInboundGroupSession() : $errorDescription") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) + } else { + return session + } + } else { + Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) + } + } + + /** + * Determine if we have the keys for a given megolm session. + * + * @param roomId room in which the message was received + * @param senderKey base64-encoded curve25519 key of the sender + * @param sessionId session identifier + * @return true if the unbound session keys are known. + */ + fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { + return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt new file mode 100644 index 0000000000000000000000000000000000000000..9991115f2802cc4a709e0e5de15fb4e742b254dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * The type of object we use for importing and exporting megolm session data. + */ +@JsonClass(generateAdapter = true) +data class MegolmSessionData( + /** + * The algorithm used. + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * Unique id for the session. + */ + @Json(name = "session_id") + val sessionId: String? = null, + + /** + * Sender's Curve25519 device key. + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * Room this session is used in. + */ + @Json(name = "room_id") + val roomId: String? = null, + + /** + * Base64'ed key data. + */ + @Json(name = "session_key") + val sessionKey: String? = null, + + /** + * Other keys the sender claims. + */ + @Json(name = "sender_claimed_keys") + val senderClaimedKeys: Map<String, String>? = null, + + // This is a shortcut for sender_claimed_keys.get("ed25519") + // Keep it for compatibility reason. + @Json(name = "sender_claimed_ed25519_key") + val senderClaimedEd25519Key: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + @Json(name = "forwarding_curve25519_key_chain") + val forwardingCurve25519KeyChain: List<String>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..092ab672a6794466713eae5e68c66e1bd076b860 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class MyDeviceInfoHolder @Inject constructor( + // The credentials, + credentials: Credentials, + // the crypto store + cryptoStore: IMXCryptoStore, + // Olm device + olmDevice: MXOlmDevice +) { + // Our device keys + /** + * my device info + */ + val myDevice: CryptoDeviceInfo + + init { + + val keys = HashMap<String, String>() + +// TODO it's a bit strange, why not load from DB? + if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { + keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! + } + + if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) { + keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! + } + +// myDevice.keys = keys +// +// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() + + // TODO hwo to really check cross signed status? + // + val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false +// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true) + + myDevice = CryptoDeviceInfo( + credentials.deviceId!!, + credentials.userId, + keys = keys, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + trustLevel = DeviceTrustLevel(crossSigned, true) + ) + + // Add our own deviceinfo to the store + val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) + + val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap() + + myDevices[myDevice.deviceId] = myDevice + + cryptoStore.storeUserDevices(credentials.userId, myDevices) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..19a8468e9c4bd5d1f8c9fd8d7006e28194b334fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto + +interface NewSessionListener { + fun onNewSession(roomId: String?, senderKey: String, sessionId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt new file mode 100644 index 0000000000000000000000000000000000000000..e59fe10c822dbc1cd6a9209d728ce3a7705850b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import javax.inject.Inject + +internal class ObjectSigner @Inject constructor(private val credentials: Credentials, + private val olmDevice: MXOlmDevice) { + + /** + * Sign Object + * + * Example: + * <pre> + * { + * "[MY_USER_ID]": { + * "ed25519:[MY_DEVICE_ID]": "sign(str)" + * } + * } + * </pre> + * + * @param strToSign the String to sign and to include in the Map + * @return a Map (see example) + */ + fun signObject(strToSign: String): Map<String, Map<String, String>> { + val result = HashMap<String, Map<String, String>>() + + val content = HashMap<String, String>() + + content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign) + ?: "" // null reported by rageshake if happens during logout + + result[credentials.userId] = content + + return result + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt new file mode 100644 index 0000000000000000000000000000000000000000..e37c2df69e1db5edaf5043136b1d5d08be86f722 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmAccount +import timber.log.Timber +import javax.inject.Inject +import kotlin.math.floor +import kotlin.math.min + +@SessionScope +internal class OneTimeKeysUploader @Inject constructor( + private val olmDevice: MXOlmDevice, + private val objectSigner: ObjectSigner, + private val uploadKeysTask: UploadKeysTask +) { + // tell if there is a OTK check in progress + private var oneTimeKeyCheckInProgress = false + + // last OTK check timestamp + private var lastOneTimeKeyCheck: Long = 0 + private var oneTimeKeyCount: Int? = null + + /** + * Stores the current one_time_key count which will be handled later (in a call of + * _onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param currentCount the new count + */ + fun updateOneTimeKeyCount(currentCount: Int) { + oneTimeKeyCount = currentCount + } + + /** + * Check if the OTK must be uploaded. + */ + suspend fun maybeUploadOneTimeKeys() { + if (oneTimeKeyCheckInProgress) { + Timber.v("maybeUploadOneTimeKeys: already in progress") + return + } + if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { + // we've done a key upload recently. + Timber.v("maybeUploadOneTimeKeys: executed too recently") + return + } + + lastOneTimeKeyCheck = System.currentTimeMillis() + oneTimeKeyCheckInProgress = true + + // We then check how many keys we can store in the Account object. + val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() + + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't received a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() + val oneTimeKeyCountFromSync = oneTimeKeyCount + if (oneTimeKeyCountFromSync != null) { + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of engineering compromise to balance all of + // these factors. + try { + val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) + Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") + } finally { + oneTimeKeyCheckInProgress = false + } + } else { + Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync") + oneTimeKeyCheckInProgress = false + lastOneTimeKeyCheck = 0 + } + } + + /** + * Upload some the OTKs. + * + * @param keyCount the key count + * @param keyLimit the limit + * @return the number of uploaded keys + */ + private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + return 0 + } + val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) + olmDevice.generateOneTimeKeys(keysThisLoop) + val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) + olmDevice.markKeysAsPublished() + + if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { + // Maybe upload other keys + return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + } else { + Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") + throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") + } + } + + /** + * Upload curve25519 one time keys. + */ + private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse { + val oneTimeJson = mutableMapOf<String, Any>() + + val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() + + curve25519Map.forEach { (key_id, value) -> + val k = mutableMapOf<String, Any>() + k["key"] = value + + // the key is also signed + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) + + k["signatures"] = objectSigner.signObject(canonicalJson) + + oneTimeJson["signed_curve25519:$key_id"] = k + } + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + val uploadParams = UploadKeysTask.Params(null, oneTimeJson) + return uploadKeysTask.execute(uploadParams) + } + + companion object { + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5 + + // frequency with which to check & upload one-time keys + private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60 * 1000).toLong() // one minute + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..34661fcc21ab6421314a0a3c7b3b836de668ae44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +interface OutgoingGossipingRequest { + var recipients: Map<String, List<String>> + var requestId: String + var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // var cancellationTxnId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt new file mode 100755 index 0000000000000000000000000000000000000000..030560b77ffd4b2379f4d7be232b7474fc6ed8dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class OutgoingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val gossipingWorkManager: GossipingWorkManager) { + + /** + * Send off a room key request, if we haven't already done so. + * + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param requestBody requestBody + * @param recipients recipients + */ + fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { + // Don't resend if it's already done, you need to cancel first (reRequest) + if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { + Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // A bit dirty, but for better stability give other party some time to mark + // devices trusted :/ + delay(1500) + cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { + // TODO check if there is already one that is being sent? + if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) { + Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + /** + * Cancel room key requests, if any match the given details + * + * @param requestBody requestBody + */ + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, false) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + */ + fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, true) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + * @param andResend true to resend the key request + */ + private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { + val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + ?: // no request was made for this key + return Unit.also { + Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody") + } + + sendOutgoingRoomKeyRequestCancellation(req, andResend) + } + + /** + * Send the outgoing key request. + * + * @param request the request + */ + private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { + Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") + + val params = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request as? OutgoingRoomKeyRequest, + secretShareRequest = request as? OutgoingSecretRequest + ) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) + val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } + + /** + * Given a OutgoingRoomKeyRequest, cancel it and delete the request record + * + * @param request the request + */ + private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) { + Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request") + val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) + + val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + + if (resend) { + val reSendParams = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId()) + ) + val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true) + gossipingWorkManager.postWork(reSendWorkRequest) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt new file mode 100755 index 0000000000000000000000000000000000000000..f27338b7121e278187b6d24cba20cd02f9aa7793 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +data class OutgoingRoomKeyRequest( + // RequestBody + var requestBody: RoomKeyRequestBody?, + // list of recipients for the request + override var recipients: Map<String, List<String>>, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + override var requestId: String, // current state of this request + override var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // override var cancellationTxnId: String? = null +) : OutgoingGossipingRequest { + + /** + * Used only for log. + * + * @return the room id. + */ + val roomId: String? + get() = if (null != requestBody) { + requestBody!!.roomId + } else null + + /** + * Used only for log. + * + * @return the session id + */ + val sessionId: String? + get() = if (null != requestBody) { + requestBody!!.sessionId + } else null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt new file mode 100755 index 0000000000000000000000000000000000000000..6b51b42b53f029344be4b10af3852a89b3c80f47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.JsonClass + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +class OutgoingSecretRequest( + // Secret Name + val secretName: String?, + // list of recipients for the request + override var recipients: Map<String, List<String>>, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + override var requestId: String, + // current state of this request + override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { + + // transaction id for the cancellation, if any +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..e574627d39f86c6db9dafa76615234b21712da5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class RoomDecryptorProvider @Inject constructor( + private val olmDecryptionFactory: MXOlmDecryptionFactory, + private val megolmDecryptionFactory: MXMegolmDecryptionFactory +) { + + // A map from algorithm to MXDecrypting instance, for each room + private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap() + + private val newSessionListeners = ArrayList<NewSessionListener>() + + fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + + /** + * Get a decryptor for a given room and algorithm. + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @param roomId the room id + * @param algorithm the crypto algorithm + * @return the decryptor + * // TODO Create another method for the case of roomId is null + */ + fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { + // sanity check + if (algorithm.isNullOrEmpty()) { + Timber.e("## getRoomDecryptor() : null algorithm") + return null + } + if (roomId != null && roomId.isNotEmpty()) { + synchronized(roomDecryptors) { + val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() } + val alg = decryptors[algorithm] + if (alg != null) { + return alg + } + } + } + val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm) + if (decryptingClass) { + val alg = when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply { + this.newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + // PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor + newSessionListeners.forEach { + try { + it.onNewSession(roomId, senderKey, sessionId) + } catch (e: Throwable) { + } + } + } + } + } + else -> olmDecryptionFactory.create() + } + if (!roomId.isNullOrEmpty()) { + synchronized(roomDecryptors) { + roomDecryptors[roomId]?.put(algorithm, alg) + } + } + return alg + } + return null + } + + fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { + if (roomId == null || algorithm == null) { + return null + } + return roomDecryptors[roomId]?.get(algorithm) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..aabe2aedec5059d05f03b57ef17483abe986fc2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class RoomEncryptorsStore @Inject constructor() { + + // MXEncrypting instance for each room. + private val roomEncryptors = mutableMapOf<String, IMXEncrypting>() + + fun put(roomId: String, alg: IMXEncrypting) { + synchronized(roomEncryptors) { + roomEncryptors.put(roomId, alg) + } + } + + fun get(roomId: String): IMXEncrypting? { + return synchronized(roomEncryptors) { + roomEncryptors[roomId] + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..db85f2c2464ce678f5ecdbb022eda2ff56feb635 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val keyShareRequest: OutgoingRoomKeyRequest? = null, + val secretShareRequest: OutgoingSecretRequest? = null + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap<Any>() + val eventType: String + val requestId: String + when { + params.keyShareRequest != null -> { + eventType = EventType.ROOM_KEY_REQUEST + requestId = params.keyShareRequest.requestId + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.keyShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = params.keyShareRequest.requestBody + ) + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.keyShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + params.secretShareRequest != null -> { + eventType = EventType.REQUEST_SECRET + requestId = params.secretShareRequest.requestId + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.secretShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + secretName = params.secretShareRequest.secretName + ) + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.secretShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + else -> { + return Result.success(errorOutputData).also { + Timber.e("Unknown empty gossiping request: $params") + } + } + } + try { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = eventType, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3eb476b51d5e2b99b540b9332034c359061ae0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val secretValue: String, + val request: IncomingSecretShareRequest + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + @Inject lateinit var messageEncrypter: MessageEncrypter + @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val eventType: String = EventType.SEND_SECRET + + val toDeviceContent = SecretSendEventContent( + requestId = params.request.requestId ?: "", + secretValue = params.secretValue + ) + + val requestingUserId = params.request.userId ?: "" + val requestingDeviceId = params.request.deviceId ?: "" + val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) + ?: return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") + } + + val sendToDeviceMap = MXUsersDevicesMap<Any>() + + val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("no session with this device, probably because there were no one-time keys.") + } + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to toDeviceContent.toContent() + ) + + try { + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) + } catch (failure: Throwable) { + Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") + } + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + try { + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ENCRYPTED, + contentMap = sendToDeviceMap, + transactionId = localId + ) + ) + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt new file mode 100644 index 0000000000000000000000000000000000000000..e69cac5a5e848800002296d24332903fd9627c3e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import timber.log.Timber +import javax.inject.Inject + +internal class EnsureOlmSessionsForDevicesAction @Inject constructor( + private val olmDevice: MXOlmDevice, + private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { + + suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> { + val devicesWithoutSession = ArrayList<CryptoDeviceInfo>() + + val results = MXUsersDevicesMap<MXOlmSessionResult>() + + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { + val deviceId = deviceInfo.deviceId + val key = deviceInfo.identityKey() + + val sessionId = olmDevice.getSessionId(key!!) + + if (sessionId.isNullOrEmpty() || force) { + devicesWithoutSession.add(deviceInfo) + } + + val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) + results.setObject(userId, deviceId, olmSessionResult) + } + } + + if (devicesWithoutSession.size == 0) { + return results + } + + // Prepare the request for claiming one-time keys + val usersDevicesToClaim = MXUsersDevicesMap<String>() + + val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE + + for (device in devicesWithoutSession) { + usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm) + } + + // TODO: this has a race condition - if we try to send another message + // while we are claiming a key, we will end up claiming two and setting up + // two sessions. + // + // That should eventually resolve itself, but it's poor form. + + Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") + + val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) + val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) + Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { + var oneTimeKey: MXKey? = null + val deviceIds = oneTimeKeys.getUserDeviceIds(userId) + if (null != deviceIds) { + for (deviceId in deviceIds) { + val olmSessionResult = results.getObject(userId, deviceId) + if (olmSessionResult!!.sessionId != null && !force) { + // We already have a result for this device + continue + } + val key = oneTimeKeys.getObject(userId, deviceId) + if (key?.type == oneTimeKeyAlgorithm) { + oneTimeKey = key + } + if (oneTimeKey == null) { + Timber.v("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId) + continue + } + // Update the result for this device in results + olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + } + } + } + } + return results + } + + private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { + var sessionId: String? = null + + val deviceId = deviceInfo.deviceId + val signKeyId = "ed25519:$deviceId" + val signature = oneTimeKey.signatureForUserId(userId, signKeyId) + + if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) { + var isVerified = false + var errorMessage: String? = null + + try { + olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + // Check one-time key signature + if (isVerified) { + sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) + + if (!sessionId.isNullOrEmpty()) { + Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId + + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + } else { + // Possibly a bad key + Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") + } + } else { + Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + + ":" + deviceId + " Error " + errorMessage) + } + } + + return sessionId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt new file mode 100644 index 0000000000000000000000000000000000000000..270240f912bfd0de63c7d19ddff2813bce7b2d64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber +import javax.inject.Inject + +internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { + + /** + * Try to make sure we have established olm sessions for the given users. + * @param users a list of user ids. + */ + suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> { + Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") + val devicesByUser = users.associateWith { userId -> + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() + + devices.filter { + // Don't bother setting up session to ourself + it.identityKey() != olmDevice.deviceCurve25519Key + // Don't bother setting up sessions with blocked users + && !(it.trustLevel?.isVerified() ?: false) + } + } + return ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9da459d7d035ee2c029ee760da23b6d914afbda --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber +import javax.inject.Inject + +internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val cryptoStore: IMXCryptoStore) { + + /** + * Import a list of megolm session keys. + * Must be call on the crypto coroutine thread + * + * @param megolmSessionsData megolm sessions. + * @param backUpKeys true to back up them to the homeserver. + * @param progressListener the progress listener + * @return import room keys result + */ + @WorkerThread + fun handle(megolmSessionsData: List<MegolmSessionData>, + fromBackup: Boolean, + progressListener: ProgressListener?): ImportRoomKeysResult { + val t0 = System.currentTimeMillis() + + val totalNumbersOfKeys = megolmSessionsData.size + var lastProgress = 0 + var totalNumbersOfImportedKeys = 0 + + progressListener?.onProgress(0, 100) + val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) + + megolmSessionsData.forEachIndexed { cpt, megolmSessionData -> + val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) + + if (null != decrypting) { + try { + val sessionId = megolmSessionData.sessionId + Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + + totalNumbersOfImportedKeys++ + + // cancel any outstanding room key requests for this session + val roomKeyRequestBody = RoomKeyRequestBody( + algorithm = megolmSessionData.algorithm, + roomId = megolmSessionData.roomId, + senderKey = megolmSessionData.senderKey, + sessionId = megolmSessionData.sessionId + ) + + outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + + // Have another go at decrypting events sent with this session + decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) + } catch (e: Exception) { + Timber.e(e, "## importRoomKeys() : onNewSession failed") + } + } + + if (progressListener != null) { + val progress = 100 * (cpt + 1) / totalNumbersOfKeys + + if (lastProgress != progress) { + lastProgress = progress + + progressListener.onProgress(progress, 100) + } + } + } + + // Do not back up the key if it comes from a backup recovery + if (fromBackup) { + cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + } + + val t1 = System.currentTimeMillis() + + Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + + return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt new file mode 100644 index 0000000000000000000000000000000000000000..c654622ffb77f84a71ebcbdbc890f51402ab7dbf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertToUTF8 +import timber.log.Timber +import javax.inject.Inject + +internal class MessageEncrypter @Inject constructor( + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val olmDevice: MXOlmDevice) { + /** + * Encrypt an event payload for a list of devices. + * This method must be called from the getCryptoHandler() thread. + * + * @param payloadFields fields to include in the encrypted payload. + * @param deviceInfos list of device infos to encrypt for. + * @return the content for an m.room.encrypted event. + */ + fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage { + val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } + + val payloadJson = payloadFields.toMutableMap() + + payloadJson["sender"] = userId + payloadJson["sender_device"] = deviceId!! + + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!) + + val ciphertext = mutableMapOf<String, Any>() + + for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { + val sessionId = olmDevice.getSessionId(deviceKey) + + if (!sessionId.isNullOrEmpty()) { + Timber.v("Using sessionid $sessionId for device $deviceKey") + + payloadJson["recipient"] = deviceInfo.userId + payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) + + val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) + ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!! + } + } + + return EncryptedMessage( + algorithm = MXCRYPTO_ALGORITHM_OLM, + senderKey = olmDevice.deviceCurve25519Key, + cipherText = ciphertext + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5c00c363240cb9ff95348f664abd28e6340c7e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId +import timber.log.Timber +import javax.inject.Inject + +internal class SetDeviceVerificationAction @Inject constructor( + private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, + private val defaultKeysBackupService: DefaultKeysBackupService) { + + fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + val device = cryptoStore.getUserDevice(userId, deviceId) + + // Sanity check + if (null == device) { + Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId") + return + } + + if (device.isVerified != trustLevel.isVerified()) { + if (userId == this.userId) { + // If one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + defaultKeysBackupService.checkAndStartKeysBackup() + } + } + + if (device.trustLevel != trustLevel) { + device.trustLevel = trustLevel + cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt new file mode 100644 index 0000000000000000000000000000000000000000..76efc4d77f90a66abccdd434d234f3cc3b752212 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService + +/** + * An interface for decrypting data + */ +internal interface IMXDecrypting { + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the decryption information, or an error + */ + @Throws(MXCryptoError::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + + /** + * Handle a key event. + * + * @param event the key event. + */ + fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + fun onNewSession(senderKey: String, sessionId: String) {} + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param request keyRequest + * @return true if we have the keys and could (theoretically) share + */ + fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false + + /** + * Send the response to a room key request. + * + * @param request keyRequest + */ + fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} + + fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} + + fun requestKeysForEvent(event: Event, withHeld: Boolean) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt new file mode 100644 index 0000000000000000000000000000000000000000..60a5d7be7a7a8e5e5f8dfbde73691c2e26cb3193 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * An interface for encrypting data + */ +internal interface IMXEncrypting { + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param userIds the room members the event will be sent to. + * @return the encrypted content + */ + suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content + + /** + * In Megolm, each recipient maintains a record of the ratchet value which allows + * them to decrypt any messages sent in the session after the corresponding point + * in the conversation. If this value is compromised, an attacker can similarly + * decrypt past messages which were encrypted by a key derived from the + * compromised or subsequent ratchet values. This gives 'partial' forward + * secrecy. + * + * To mitigate this issue, the application should offer the user the option to + * discard historical conversations, by winding forward any stored ratchet values, + * or discarding sessions altogether. + */ + fun discardSessionKey() + + /** + * Re-shares a session key with devices if the key has already been + * sent to them. + * + * @param sessionId The id of the outbound session to share. + * @param userId The id of the user who owns the target device. + * @param deviceId The id of the target device. + * @param senderKey The key of the originating device for the session. + * + * @return true in case of success + */ + suspend fun reshareKey(sessionId: String, + userId: String, + deviceId: String, + senderKey: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..844cb38858071f45b2db01ef9e0c3e45e648258b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent + +internal interface IMXWithHeldExtension { + fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt new file mode 100644 index 0000000000000000000000000000000000000000..423c883927acbc0f94e32e0c0096085fbb1cd0c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class MXMegolmDecryption(private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : IMXDecrypting, IMXWithHeldExtension { + + var newSessionListener: NewSessionListener? = null + + /** + * Events which we couldn't decrypt due to unknown sessions / indexes: map from + * senderKey|sessionId to timelines to list of MatrixEvents. + */ +// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap() + + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + // If cross signing is enabled, we don't send request until the keys are trusted + // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once + val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true + return decryptEvent(event, timeline, requestOnFail) + } + + @Throws(MXCryptoError::class) + private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { + Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail") + if (event.roomId.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + + val encryptedEventContent = event.content.toModel<EncryptedEventContent>() + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + + if (encryptedEventContent.senderKey.isNullOrBlank() + || encryptedEventContent.sessionId.isNullOrBlank() + || encryptedEventContent.ciphertext.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + + return runCatching { + olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, + event.roomId, + timeline, + encryptedEventContent.sessionId, + encryptedEventContent.senderKey) + } + .fold( + { olmDecryptionResult -> + // the decryption succeeds + if (olmDecryptionResult.payload != null) { + MXEventDecryptionResult( + clearEvent = olmDecryptionResult.payload, + senderCurve25519Key = olmDecryptionResult.senderKey, + claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain + .orEmpty() + ) + } else { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + }, + { throwable -> + if (throwable is MXCryptoError.OlmError) { + // TODO Check the value of .message + if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { + // addEventToPendingList(event, timeline) + // The session might has been partially withheld (and only pass ratcheted) + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } + + if (requestKeysOnFail) { + requestKeysForEvent(event, false) + } + } + + val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) + val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) + + throw MXCryptoError.Base( + MXCryptoError.ErrorType.OLM, + reason, + detailedReason) + } + if (throwable is MXCryptoError.Base) { + if ( + /** if the session is unknown*/ + throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + ) { + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } else { + // This is un-used in riotX SDK, not sure if needed + // addEventToPendingList(event, timeline) + if (requestKeysOnFail) { + requestKeysForEvent(event, false) + } + } + } + } + throw throwable + } + ) + } + + /** + * Helper for the real decryptEvent and for _retryDecryption. If + * requestKeysOnFail is true, we'll send an m.room_key_request when we fail + * to decrypt the event due to missing megolm keys. + * + * @param event the event + */ + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { + val sender = event.senderId ?: return + val encryptedEventContent = event.content.toModel<EncryptedEventContent>() + val senderDevice = encryptedEventContent?.deviceId ?: return + + val recipients = if (event.senderId == userId || withHeld) { + mapOf( + userId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + userId to listOf("*"), + sender to listOf(senderDevice) + ) + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + + outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) + } + +// /** +// * Add an event to the list of those we couldn't decrypt the first time we +// * saw them. +// * +// * @param event the event to try to decrypt later +// * @param timelineId the timeline identifier +// */ +// private fun addEventToPendingList(event: Event, timelineId: String) { +// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return +// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" +// +// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } +// val events = timeline.getOrPut(timelineId) { ArrayList() } +// +// if (event !in events) { +// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") +// events.add(event) +// } +// } + + /** + * Handle a key event. + * + * @param event the key event. + */ + override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { + Timber.v("## CRYPTO | onRoomKeyEvent()") + var exportFormat = false + val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return + + var senderKey: String? = event.getSenderKey() + var keysClaimed: MutableMap<String, String> = HashMap() + val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() + + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { + Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields") + return + } + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + Timber.v("## CRYPTO | onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" + + " sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}") + val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() + ?: return + + forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { + forwardingCurve25519KeyChain.addAll(it) + } + + if (senderKey == null) { + Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field") + return + } + + forwardingCurve25519KeyChain.add(senderKey) + + exportFormat = true + senderKey = forwardedRoomKeyContent.senderKey + if (null == senderKey) { + Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") + return + } + + if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { + Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field") + return + } + + keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key + } else { + Timber.v("## CRYPTO | onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId + + " sessionKey " + roomKeyContent.sessionKey) // from " + event); + + if (null == senderKey) { + Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + return + } + + // inherit the claimed ed25519 key from the setup message + keysClaimed = event.getKeysClaimed().toMutableMap() + } + + Timber.e("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") + val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, + roomKeyContent.sessionKey, + roomKeyContent.roomId, + senderKey, + forwardingCurve25519KeyChain, + keysClaimed, + exportFormat) + + if (added) { + defaultKeysBackupService.maybeBackupKeys() + + val content = RoomKeyRequestBody( + algorithm = roomKeyContent.algorithm, + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey + ) + + outgoingGossipingRequestManager.cancelRoomKeyRequest(content) + + onNewSession(senderKey, roomKeyContent.sessionId) + } + } + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + override fun onNewSession(senderKey: String, sessionId: String) { + Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey") + newSessionListener?.onNewSession(null, senderKey, sessionId) + } + + override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { + val roomId = request.requestBody?.roomId ?: return false + val senderKey = request.requestBody.senderKey ?: return false + val sessionId = request.requestBody.sessionId ?: return false + return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) + } + + override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { + // sanity checks + if (request.requestBody == null) { + return + } + val userId = request.userId ?: return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { deviceListManager.downloadKeys(listOf(userId), false) } + .mapCatching { + val deviceId = request.deviceId + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") + if (deviceInfo == null) { + throw RuntimeException() + } else { + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val body = request.requestBody + val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return@mapCatching + } + Timber.v("## CRYPTO | shareKeysWithDevice() : sharing keys for session" + + " ${body.senderKey}|${body.sessionId} with device $userId:$deviceId") + + val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) + runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } + .fold( + { + // TODO + payloadJson["content"] = it.exportKeys() ?: "" + }, + { + // TODO + } + + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap<Any>() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.v("## CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + } + } + + override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.addWithHeldMegolmSession(withHeldInfo) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7b2919dbe63bc7d5f0dda1c28ff59aaee757528 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +internal class MXMegolmDecryptionFactory @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) { + + fun create(): MXMegolmDecryption { + return MXMegolmDecryption( + userId, + olmDevice, + deviceListManager, + outgoingGossipingRequestManager, + messageEncrypter, + ensureOlmSessionsForDevicesAction, + cryptoStore, + sendToDeviceTask, + coroutineDispatchers, + cryptoCoroutineScope) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c2dfc9e5d1bee29048eef8e354672693863e85e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -0,0 +1,440 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.matrix.android.sdk.internal.crypto.model.forEach +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertToUTF8 +import timber.log.Timber + +internal class MXMegolmEncryption( + // The id of the room we will be sending to. + private val roomId: String, + private val olmDevice: MXOlmDevice, + private val defaultKeysBackupService: DefaultKeysBackupService, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val credentials: Credentials, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor +) : IMXEncrypting { + + // OutboundSessionInfo. Null if we haven't yet started setting one up. Note + // that even if this is non-null, it may not be ready for use (in which + // case outboundSession.shareOperation will be non-null.) + private var outboundSession: MXOutboundSessionInfo? = null + + // Default rotation periods + // TODO: Make it configurable via parameters + // Session rotation periods + private var sessionRotationPeriodMsgs: Int = 100 + private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000 + + override suspend fun encryptEventContent(eventContent: Content, + eventType: String, + userIds: List<String>): Content { + val ts = System.currentTimeMillis() + Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") + val devices = getDevicesInRoom(userIds) + Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + val outboundSession = ensureOutboundSession(devices.allowedDevices) + + return encryptContent(outboundSession, eventType, eventContent) + .also { + notifyWithheldForSession(devices.withHeldDevices, outboundSession) + } + } + + private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) { + mutableListOf<Pair<UserDevice, WithHeldCode>>().apply { + devices.forEach { userId, deviceId, withheldCode -> + this.add(UserDevice(userId, deviceId) to withheldCode) + } + }.groupBy( + { it.second }, + { it.first } + ).forEach { (code, targets) -> + notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) + } + } + + override fun discardSessionKey() { + outboundSession = null + } + + /** + * Prepare a new session. + * + * @return the session description + */ + private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { + Timber.v("## CRYPTO | prepareNewSessionInRoom() ") + val sessionId = olmDevice.createOutboundGroupSession() + + val keysClaimedMap = HashMap<String, String>() + keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! + + olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, + emptyList(), keysClaimedMap, false) + + defaultKeysBackupService.maybeBackupKeys() + + return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore)) + } + + /** + * Ensure the outbound session + * + * @param devicesInRoom the devices list + */ + private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo { + Timber.v("## CRYPTO | ensureOutboundSession start") + var session = outboundSession + if (session == null + // Need to make a brand new session? + || session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) + // Determine if we have shared with anyone we shouldn't have + || session.sharedWithTooManyDevices(devicesInRoom)) { + session = prepareNewSessionInRoom() + outboundSession = session + } + val safeSession = session + val shareMap = HashMap<String, MutableList<CryptoDeviceInfo>>()/* userId */ + val userIds = devicesInRoom.userIds + for (userId in userIds) { + val deviceIds = devicesInRoom.getUserDeviceIds(userId) + for (deviceId in deviceIds!!) { + val deviceInfo = devicesInRoom.getObject(userId, deviceId) + if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) { + val devices = shareMap.getOrPut(userId) { ArrayList() } + devices.add(deviceInfo) + } + } + } + shareKey(safeSession, shareMap) + return safeSession + } + + /** + * Share the device key to a list of users + * + * @param session the session info + * @param devicesByUsers the devices map + */ + private suspend fun shareKey(session: MXOutboundSessionInfo, + devicesByUsers: Map<String, List<CryptoDeviceInfo>>) { + // nothing to send, the task is done + if (devicesByUsers.isEmpty()) { + Timber.v("## CRYPTO | shareKey() : nothing more to do") + return + } + // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) + val subMap = HashMap<String, List<CryptoDeviceInfo>>() + var devicesCount = 0 + for ((userId, devices) in devicesByUsers) { + subMap[userId] = devices + devicesCount += devices.size + if (devicesCount > 100) { + break + } + } + Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") + shareUserDevicesKey(session, subMap) + val remainingDevices = devicesByUsers - subMap.keys + shareKey(session, remainingDevices) + } + + /** + * Share the device keys of a an user + * + * @param session the session info + * @param devicesByUser the devices map + */ + private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, + devicesByUser: Map<String, List<CryptoDeviceInfo>>) { + val sessionKey = olmDevice.getSessionKey(session.sessionId) + val chainIndex = olmDevice.getMessageIndex(session.sessionId) + + val submap = HashMap<String, Any>() + submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + submap["room_id"] = roomId + submap["session_id"] = session.sessionId + submap["session_key"] = sessionKey!! + submap["chain_index"] = chainIndex + + val payload = HashMap<String, Any>() + payload["type"] = EventType.ROOM_KEY + payload["content"] = submap + + var t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | shareUserDevicesKey() : starts") + + val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + Timber.v("## CRYPTO | shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + val contentMap = MXUsersDevicesMap<Any>() + var haveTargets = false + val userIds = results.userIds + for (userId in userIds) { + val devicesToShareWith = devicesByUser[userId] + for ((deviceID) in devicesToShareWith!!) { + val sessionResult = results.getObject(userId, deviceID) + if (sessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + + // MSC 2399 + // send withheld m.no_olm: an olm session could not be established. + // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + notifyKeyWithHeld( + listOf(UserDevice(userId, deviceID)), + session.sessionId, + olmDevice.deviceCurve25519Key, + WithHeldCode.NO_OLM + ) + + continue + } + Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) + haveTargets = true + } + } + + // Add the devices we have shared with to session.sharedWithDevices. + // we deliberately iterate over devicesByUser (ie, the devices we + // attempted to share with) rather than the contentMap (those we did + // share with), because we don't want to try to claim a one-time-key + // for dead devices on every message. + for ((userId, devicesToShareWith) in devicesByUser) { + for ((deviceId) in devicesToShareWith) { + session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) + } + } + + if (haveTargets) { + t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | shareUserDevicesKey() : has target") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) + try { + sendToDeviceTask.execute(sendToDeviceParams) + Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + } catch (failure: Throwable) { + // What to do here... + Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + } + } else { + Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + } + } + + private fun notifyKeyWithHeld(targets: List<UserDevice>, sessionId: String, senderKey: String?, code: WithHeldCode) { + val withHeldContent = RoomKeyWithHeldContent( + roomId = roomId, + senderKey = senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sessionId = sessionId, + codeString = code.value + ) + val params = SendToDeviceTask.Params( + EventType.ROOM_KEY_WITHHELD, + MXUsersDevicesMap<Any>().apply { + targets.forEach { + setObject(it.userId, it.deviceId, withHeldContent) + } + } + ) + sendToDeviceTask.configureWith(params) { + callback = object : MatrixCallback<Unit> { + override fun onFailure(failure: Throwable) { + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + } + } + }.executeBy(taskExecutor) + } + + /** + * process the pending encryptions + */ + private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { + // Everything is in place, encrypt all pending events + val payloadJson = HashMap<String, Any>() + payloadJson["room_id"] = roomId + payloadJson["type"] = eventType + payloadJson["content"] = eventContent + + // Get canonical Json from + + val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) + val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString) + + val map = HashMap<String, Any>() + map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + map["sender_key"] = olmDevice.deviceCurve25519Key!! + map["ciphertext"] = ciphertext!! + map["session_id"] = session.sessionId + + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + map["device_id"] = credentials.deviceId!! + session.useCount++ + return map + } + + /** + * Get the list of devices which can encrypt data to. + * This method must be called in getDecryptingThreadHandler() thread. + * + * @param userIds the user ids whose devices must be checked. + */ + private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo { + // We are happy to use a cached version here: we assume that if we already + // have a list of the user's devices, then we already share an e2e room + // with them, which means that they will have announced any new devices via + // an m.new_device. + val keys = deviceListManager.downloadKeys(userIds, false) + val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() + || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + + val devicesInRoom = DeviceInRoomInfo() + val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>() + + for (userId in keys.userIds) { + val deviceIds = keys.getUserDeviceIds(userId) ?: continue + for (deviceId in deviceIds) { + val deviceInfo = keys.getObject(userId, deviceId) ?: continue + if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { + // The device is not yet known by the user + unknownDevices.setObject(userId, deviceId, deviceInfo) + continue + } + if (deviceInfo.isBlocked) { + // Remove any blocked devices + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED) + continue + } + + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) + continue + } + + if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) { + // Don't bother sending to ourself + continue + } + devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo) + } + } + if (unknownDevices.isEmpty) { + return devicesInRoom + } else { + throw MXCryptoError.UnknownDevice(unknownDevices) + } + } + + override suspend fun reshareKey(sessionId: String, + userId: String, + deviceId: String, + senderKey: String): Boolean { + Timber.d("[MXMegolmEncryption] reshareKey: $sessionId to $userId:$deviceId") + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false + .also { Timber.w("Device not found") } + + // Get the chain index of the key we previously sent this device + val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false + .also { + // Send a room key with held + notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) + Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") + } + + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) + olmSessionResult?.sessionId + ?: // no session with this device, probably because there were no one-time keys. + // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. + return false + + Timber.d("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") + + val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) + + runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // TODO + payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: "" + }, + { + // TODO + } + + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap<Any>() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + return try { + sendToDeviceTask.execute(sendToDeviceParams) + true + } catch (failure: Throwable) { + Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + false + } + } + + data class DeviceInRoomInfo( + val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(), + val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap() + ) + + data class UserDevice( + val userId: String, + val deviceId: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca7b9657ae5b0bda8df2037f4c97a98bf8288130 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal class MXMegolmEncryptionFactory @Inject constructor( + private val olmDevice: MXOlmDevice, + private val defaultKeysBackupService: DefaultKeysBackupService, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val credentials: Credentials, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor) { + + fun create(roomId: String): MXMegolmEncryption { + return MXMegolmEncryption( + roomId, + olmDevice, + defaultKeysBackupService, + cryptoStore, + deviceListManager, + ensureOlmSessionsForDevicesAction, + credentials, + sendToDeviceTask, + messageEncrypter, + warnOnUnknownDevicesRepository, + taskExecutor + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..9bdebdf24ffcb8ced3652857cebb6bb071b239b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import timber.log.Timber + +internal class MXOutboundSessionInfo( + // The id of the session + val sessionId: String, + val sharedWithHelper: SharedWithHelper) { + // When the session was created + private val creationTime = System.currentTimeMillis() + + // Number of times this session has been used + var useCount: Int = 0 + + fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { + var needsRotation = false + val sessionLifetime = System.currentTimeMillis() - creationTime + + if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { + Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms") + needsRotation = true + } + + return needsRotation + } + + /** + * Determine if this session has been shared with devices which it shouldn't have been. + * + * @param devicesInRoom the devices map + * @return true if we have shared the session with devices which aren't in devicesInRoom. + */ + fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean { + val sharedWithDevices = sharedWithHelper.sharedWithDevices() + val userIds = sharedWithDevices.userIds + + for (userId in userIds) { + if (null == devicesInRoom.getUserDeviceIds(userId)) { + Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId") + return true + } + + val deviceIds = sharedWithDevices.getUserDeviceIds(userId) + + for (deviceId in deviceIds!!) { + if (null == devicesInRoom.getObject(userId, deviceId)) { + Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId") + return true + } + } + } + + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..c018f6e275017b2281c5ae9bec9c9e747567866d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +internal class SharedWithHelper( + private val roomId: String, + private val sessionId: String, + private val cryptoStore: IMXCryptoStore) { + + fun sharedWithDevices(): MXUsersDevicesMap<Int> { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + + fun wasSharedWith(userId: String, deviceId: String): Int? { + return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex + } + + fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) { + cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4295e2cec04640ab3b3138692dbe080bfda8cb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.OlmPayloadContent +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.convertFromUTF8 +import timber.log.Timber + +internal class MXOlmDecryption( + // The olm device interface + private val olmDevice: MXOlmDevice, + // the matrix userId + private val userId: String) + : IMXDecrypting { + + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val olmEventContent = event.content.toModel<OlmEventContent>() ?: run { + Timber.e("## decryptEvent() : bad event format") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, + MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) + } + + val cipherText = olmEventContent.ciphertext ?: run { + Timber.e("## decryptEvent() : missing cipher text") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + val senderKey = olmEventContent.senderKey ?: run { + Timber.e("## decryptEvent() : missing sender key") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, + MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) + } + + val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { + Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") + throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) + } + + // The message for myUser + @Suppress("UNCHECKED_CAST") + val message = messageAny as JsonDict + + val decryptedPayload = decryptMessage(message, senderKey) + + if (decryptedPayload == null) { + Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } + val payloadString = convertFromUTF8(decryptedPayload) + + val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) + val payload = adapter.fromJson(payloadString) + + if (payload == null) { + Timber.e("## decryptEvent failed : null payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { + Timber.e("## decryptEvent() : bad olmPayloadContent format") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + if (olmPayloadContent.recipient.isNullOrBlank()) { + val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") + Timber.e("## decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) + } + + if (olmPayloadContent.recipient != userId) { + Timber.e("## decryptEvent() : Event ${event.eventId}:" + + " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, + String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) + } + + val recipientKeys = olmPayloadContent.recipient_keys ?: run { + Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + + " property; cannot prevent unknown-key attack") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) + } + + val ed25519 = recipientKeys["ed25519"] + + if (ed25519 != olmDevice.deviceEd25519Key) { + Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, + MXCryptoError.BAD_RECIPIENT_KEY_REASON) + } + + if (olmPayloadContent.sender.isNullOrBlank()) { + Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) + } + + if (olmPayloadContent.sender != event.senderId) { + Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") + throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, + String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) + } + + if (olmPayloadContent.room_id != event.roomId) { + Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, + String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)) + } + + val keys = olmPayloadContent.keys ?: run { + Timber.e("## decryptEvent failed : null keys") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + return MXEventDecryptionResult( + clearEvent = payload, + senderCurve25519Key = senderKey, + claimedEd25519Key = keys["ed25519"] + ) + } + + /** + * Attempt to decrypt an Olm message. + * + * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. + * @param message message object, with 'type' and 'body' fields. + * @return payload, if decrypted successfully. + */ + private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { + val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet() + + val messageBody = message["body"] as? String ?: return null + val messageType = when (val typeAsVoid = message["type"]) { + is Double -> typeAsVoid.toInt() + is Int -> typeAsVoid + is Long -> typeAsVoid.toInt() + else -> return null + } + + // Try each session in turn + // decryptionErrors = {}; + for (sessionId in sessionIds) { + val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) + + if (null != payload) { + Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") + return payload + } else { + val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) + + if (foundSession) { + // Decryption failed, but it was a prekey message matching this + // session, so it should have worked. + Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") + return null + } + } + } + + if (messageType != 0) { + // not a prekey message, so it should have matched an existing session, but it + // didn't work. + + if (sessionIds.isEmpty()) { + Timber.e("## decryptMessage() : No existing sessions") + } else { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + } + + return null + } + + // prekey message which doesn't match any existing sessions: make a new + // session. + val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) + + if (null == res) { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + return null + } + + Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") + + return res["payload"] + } + + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { + // nop + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..17c743fc085109de2c60c0ba0f675a2dc27dc780 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.di.UserId +import javax.inject.Inject + +internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, + @UserId private val userId: String) { + + fun create(): MXOlmDecryption { + return MXOlmDecryption( + olmDevice, + userId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt new file mode 100644 index 0000000000000000000000000000000000000000..f253ce005a2e3bf890cd4c40dd37d311d679a4d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +internal class MXOlmEncryption( + private val roomId: String, + private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val messageEncrypter: MessageEncrypter, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) + : IMXEncrypting { + + override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content { + // pick the list of recipients based on the membership list. + // + // TODO: there is a race condition here! What if a new user turns up + ensureSession(userIds) + val deviceInfos = ArrayList<CryptoDeviceInfo>() + for (userId in userIds) { + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() + for (device in devices) { + val key = device.identityKey() + if (key == olmDevice.deviceCurve25519Key) { + // Don't bother setting up session to ourself + continue + } + if (device.isBlocked) { + // Don't bother setting up sessions with blocked users + continue + } + deviceInfos.add(device) + } + } + + val messageMap = mapOf( + "room_id" to roomId, + "type" to eventType, + "content" to eventContent + ) + + messageEncrypter.encryptMessage(messageMap, deviceInfos) + return messageMap.toContent() + } + + /** + * Ensure that the session + * + * @param users the user ids list + */ + private suspend fun ensureSession(users: List<String>) { + deviceListManager.downloadKeys(users, false) + ensureOlmSessionsForUsersAction.handle(users) + } + + override fun discardSessionKey() { + // No need for olm + } + + override suspend fun reshareKey(sessionId: String, userId: String, deviceId: String, senderKey: String): Boolean { + // No need for olm + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..d80c34485412f0a283e144310880b0014cffc18e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import javax.inject.Inject + +internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val messageEncrypter: MessageEncrypter, + private val deviceListManager: DeviceListManager, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) { + + fun create(roomId: String): MXOlmEncryption { + return MXOlmEncryption( + roomId, + olmDevice, + cryptoStore, + messageEncrypter, + deviceListManager, + ensureOlmSessionsForUsersAction) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt new file mode 100755 index 0000000000000000000000000000000000000000..ba627d4c302a07df8ddf554963e9a29ba80492b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * This class represents the decryption result. + */ +@JsonClass(generateAdapter = true) +data class OlmDecryptionResult( + /** + * The decrypted payload (with properties 'type', 'content') + */ + @Json(name = "payload") val payload: JsonDict? = null, + + /** + * keys that the sender of the event claims ownership of: + * map from key type to base64-encoded key. + */ + @Json(name = "keysClaimed") val keysClaimed: Map<String, String>? = null, + + /** + * The curve25519 key that the sender of the event is known to have ownership of. + */ + @Json(name = "senderKey") val senderKey: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..a12b725efdeea5dddc9542daa54077e6a01debef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.api + +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface CryptoApi { + + /** + * Get the devices list + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") + fun getDevices(): Call<DevicesListResponse> + + /** + * Get the device info by id + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}") + fun getDeviceInfo(@Path("deviceId") deviceId: String): Call<DeviceInfo> + + /** + * Upload device and/or one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload + * + * @param body the keys to be sent. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload") + fun uploadKeys(@Body body: KeysUploadBody): Call<KeysUploadResponse> + + /** + * Download device keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") + fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse> + + /** + * CrossSigning - Uploading signing keys + * Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload. + * This endpoint requires UI Auth. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload") + fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call<KeysQueryResponse> + + /** + * CrossSigning - Uploading signatures + * Signatures of device keys can be up + * loaded using /keys/signatures/upload. + * For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key), + * her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key). + * + * The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + * + * After Alice uploads a signature for her own devices or master key, + * her signature will be included in the results of the /keys/query request when anyone requests her keys. + * However, signatures made for other users' keys, made by her user-signing key, will not be included. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload") + fun uploadSignatures(@Body params: Map<String, @JvmSuppressWildcards Any>?): Call<SignatureUploadResponse> + + /** + * Claim one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") + fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): Call<KeysClaimResponse> + + /** + * Send an event to a specific list of devices + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-sendtodevice-eventtype-txnid + * + * @param eventType the type of event to send + * @param transactionId the transaction ID for this event + * @param body the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}") + fun sendToDevice(@Path("eventType") eventType: String, + @Path("txnId") transactionId: String, + @Body body: SendToDeviceBody): Call<Unit> + + /** + * Delete a device. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the deletion parameters + */ + @HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true) + fun deleteDevice(@Path("device_id") deviceId: String, + @Body params: DeleteDeviceParams): Call<Unit> + + /** + * Update the device information. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}") + fun updateDeviceInfo(@Path("device_id") deviceId: String, + @Body params: UpdateDeviceInfoBody): Call<Unit> + + /** + * Get the update devices list from two sync token. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-keys-changes + * + * @param oldToken the start token. + * @param newToken the up-to token. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes") + fun getKeyChanges(@Query("from") oldToken: String, + @Query("to") newToken: String): Call<KeyChangesResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt new file mode 100644 index 0000000000000000000000000000000000000000..21f7c209efad282ebe4e7eedca10b5066eb1ae68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import android.os.Parcelable +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import kotlinx.android.parcel.Parcelize + +fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { + // Check the validity of some fields + if (isValid()) { + // It's valid so the data are here + return ElementToDecrypt( + iv = this.iv ?: "", + k = this.key?.k ?: "", + sha256 = this.hashes?.get("sha256") ?: "" + ) + } + + return null +} + +/** + * Represent data to decode an attachment + */ +@Parcelize +data class ElementToDecrypt( + val iv: String, + val k: String, + val sha256: String +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..721ee0639d139702f85940cc67aa7d59646e3618 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +/** + * Define the result of an encryption file + */ +internal data class EncryptionResult( + var encryptedFileInfo: EncryptedFileInfo, + var encryptedByteArray: ByteArray +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt new file mode 100755 index 0000000000000000000000000000000000000000..cec1480d7b8a270c6c5d416cf1e967ca95cc685a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import android.util.Base64 +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +internal object MXEncryptedAttachments { + private const val CRYPTO_BUFFER_SIZE = 32 * 1024 + private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" + private const val SECRET_KEY_SPEC_ALGORITHM = "AES" + private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" + + /*** + * Encrypt an attachment stream. + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param mimetype the mime type + * @return the encryption file info + */ + fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult { + val t0 = System.currentTimeMillis() + val secureRandom = SecureRandom() + + // generate a random iv key + // Half of the IV is random, the lower order bits are zeroed + // such that the counter never wraps. + // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 + val initVectorBytes = ByteArray(16) { 0.toByte() } + + val ivRandomPart = ByteArray(8) + secureRandom.nextBytes(ivRandomPart) + + System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) + + val key = ByteArray(32) + secureRandom.nextBytes(key) + + ByteArrayOutputStream().use { outputStream -> + val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var read: Int + var encodedBytes: ByteArray + + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + encodedBytes = encryptCipher.update(data, 0, read) + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + read = inputStream.read(data) + } + } + + // encrypt the latest chunk + encodedBytes = encryptCipher.doFinal() + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + + return EncryptionResult( + encryptedFileInfo = EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ), + encryptedByteArray = outputStream.toByteArray() + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + } + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param encryptedFileInfo the encryption file info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { + if (encryptedFileInfo?.isValid() != true) { + Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") + return null + } + + val elementToDecrypt = encryptedFileInfo.toElementToDecrypt() + + return decryptAttachment(attachmentStream, elementToDecrypt) + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param elementToDecrypt the elementToDecrypt info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { + // sanity checks + if (null == attachmentStream || elementToDecrypt == null) { + Timber.e("## decryptAttachment() : null stream") + return null + } + + val t0 = System.currentTimeMillis() + + ByteArrayOutputStream().use { outputStream -> + try { + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) + + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + var read: Int + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var decodedBytes: ByteArray + + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + messageDigest.update(data, 0, read) + decodedBytes = decryptCipher.update(data, 0, read) + outputStream.write(decodedBytes) + read = inputStream.read(data) + } + } + + // decrypt the last chunk + decodedBytes = decryptCipher.doFinal() + outputStream.write(decodedBytes) + + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) + + if (elementToDecrypt.sha256 != currentDigestValue) { + Timber.e("## decryptAttachment() : Digest value mismatch") + return null + } + + return ByteArrayInputStream(outputStream.toByteArray()) + .also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## decryptAttachment() failed: OOM") + } catch (e: Exception) { + Timber.e(e, "## decryptAttachment() failed") + } + } + + return null + } + + /** + * Base64 URL conversion methods + */ + + private fun base64UrlToBase64(base64Url: String): String { + return base64Url.replace('-', '+') + .replace('_', '/') + } + + internal fun base64ToBase64Url(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("\\+".toRegex(), "-") + .replace('/', '_') + .replace("=", "") + } + + private fun base64ToUnpaddedBase64(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("=", "") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..3bcbeefa91a52c1d8774e71731fefacd97b65599 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> { + data class Params( + val activeMemberUserIds: List<String>, + val isDirectRoom: Boolean + ) +} + +internal class DefaultComputeTrustTask @Inject constructor( + private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) : ComputeTrustTask { + + override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { + // The set of “all users†depends on the type of room: + // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room + val listToCheck = if (params.isDirectRoom) { + params.activeMemberUserIds.filter { it != userId } + } else { + params.activeMemberUserIds + } + + val allTrustedUserIds = listToCheck + .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } + + if (allTrustedUserIds.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // If all devices of all verified users are trusted -> green + // else -> black + allTrustedUserIds + .mapNotNull { cryptoStore.getUserDeviceList(it) } + .flatten() + .let { allDevices -> + if (getMyCrossSigningKeys() != null) { + allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } + } else { + // Legacy method + allDevices.any { !it.isVerified } + } + } + .let { hasWarning -> + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } + } + } + } + } + + private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return cryptoStore.getCrossSigningInfo(otherUserId) + } + + private fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return cryptoStore.getMyCrossSigningInfo() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt new file mode 100644 index 0000000000000000000000000000000000000000..8cd4a6b8e8c9808bb0ec715a033281b1831eb423 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -0,0 +1,747 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.withoutPrefix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultCrossSigningService @Inject constructor( + @UserId private val userId: String, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val initializeCrossSigningTask: InitializeCrossSigningTask, + private val uploadSignaturesTask: UploadSignaturesTask, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { + + private var olmUtility: OlmUtility? = null + + private var masterPkSigning: OlmPkSigning? = null + private var userPkSigning: OlmPkSigning? = null + private var selfSigningPkSigning: OlmPkSigning? = null + + init { + try { + olmUtility = OlmUtility() + + // Try to get stored keys if they exist + cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo -> + Timber.i("## CrossSigning - Found Existing self signed keys") + Timber.i("## CrossSigning - Checking if private keys are known") + + cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> + privateKeysInfo.master + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning = pkSigning + Timber.i("## CrossSigning - Loading master key success") + } else { + Timber.w("## CrossSigning - Public master key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + privateKeysInfo.user + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading User Signing key success") + } else { + Timber.w("## CrossSigning - Public User key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + privateKeysInfo.selfSigned + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading Self Signing key success") + } else { + Timber.w("## CrossSigning - Public Self Signing key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + } + + // Recover local trust in case private key are there? + setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + } + } catch (e: Throwable) { + // Mmm this kind of a big issue + Timber.e(e, "Failed to initialize Cross Signing") + } + + deviceListManager.addListener(this) + } + + fun release() { + olmUtility?.releaseUtility() + listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } + deviceListManager.removeListener(this) + } + + protected fun finalize() { + release() + } + + /** + * - Make 3 key pairs (MSK, USK, SSK) + * - Save the private keys with proper security + * - Sign the keys and upload them + * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures + */ + override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) { + Timber.d("## CrossSigning initializeCrossSigning") + + val params = InitializeCrossSigningTask.Params( + authParams = authParams + ) + initializeCrossSigningTask.configureWith(params) { + this.callbackThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "Error in initializeCrossSigning()") + callback.onFailure(failure) + } + + override fun onSuccess(data: InitializeCrossSigningTask.Result) { + val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + cryptoStore.setMyCrossSigningInfo(crossSigningInfo) + setUserKeysAsTrusted(userId, true) + cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) + masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } + userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } + selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } + + callback.onSuccess(Unit) + } + } + }.executeBy(taskExecutor) + } + + override fun onSecretMSKGossip(mskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known") + } + + mskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning?.releaseSigning() + masterPkSigning = pkSigning + Timber.i("## CrossSigning - Loading MSK success") + cryptoStore.storeMSKPrivateKey(mskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } + } + + override fun onSecretSSKGossip(sskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") + } + + sskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading SSK success") + cryptoStore.storeSSKPrivateKey(sskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } + } + + override fun onSecretUSKGossip(uskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretUSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") + } + + uskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading USK success") + cryptoStore.storeUSKPrivateKey(uskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + } + + override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String? + ): UserTrustResult { + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + var masterKeyIsTrusted = false + var userKeyIsTrusted = false + var selfSignedKeyIsTrusted = false + + masterKeyPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning?.releaseSigning() + masterPkSigning = pkSigning + masterKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + uskKeyPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + userKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + sskPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + selfSignedKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) { + return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo) + } else { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + val checkSelfTrust = checkSelfTrust() + if (checkSelfTrust.isVerified()) { + cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) + setUserKeysAsTrusted(userId, true) + } + return checkSelfTrust + } + } + + /** + * + * â”â”â”â”â”â”â”â”â”┓ â”â”â”â”â”â”â”â”â”┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * â”—â”â”â”â”â”â”â”â”â”› â”—â”â”â”â”â”â”â”â”â”› + * MSK ┌────────────▶ MSK + * │ + * │ │ + * │ SSK │ + * │ │ + * │ │ + * └──▶ USK ────────────┘ + */ + override fun isUserTrusted(otherUserId: String): Boolean { + return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + } + + override fun isCrossSigningVerified(): Boolean { + return checkSelfTrust().isVerified() + } + + /** + * Will not force a download of the key, but will verify signatures trust chain + */ + override fun checkUserTrust(otherUserId: String): UserTrustResult { + Timber.v("## CrossSigning checkUserTrust for $otherUserId") + if (otherUserId == userId) { + return checkSelfTrust() + } + // I trust a user if I trust his master key + // I can trust the master key if it is signed by my user key + // TODO what if the master key is signed by a device key that i have verified + + // First let's get my user key + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myUserKey = myCrossSigningInfo?.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + if (!myCrossSigningInfo.isTrusted()) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + // Let's get the other user master key + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) + + val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") + + if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") + return UserTrustResult.KeyNotSigned(otherMasterKey) + } + + // Check that Alice USK signature of Bob MSK is valid + try { + olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) + } + + return UserTrustResult.Success + } + + private fun checkSelfTrust(): UserTrustResult { + // Special case when it's me, + // I have to check that MSK -> USK -> SSK + // and that MSK is trusted (i know the private key, or is signed by a trusted device) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myMasterKey = myCrossSigningInfo?.masterKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + // Is the master key trusted + // 1) check if I know the private key + val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() + ?.master + ?.fromBase64() + + var isMaterKeyTrusted = false + if (myMasterKey.trustLevel?.locallyVerified == true) { + isMaterKeyTrusted = true + } else if (masterPrivateKey != null) { + // Check if private match public + var olmPkSigning: OlmPkSigning? = null + try { + olmPkSigning = OlmPkSigning() + val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) + isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK + } catch (failure: Throwable) { + Timber.e(failure) + } + olmPkSigning?.releaseSigning() + } else { + // Maybe it's signed by a locally trusted device? + myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + val potentialDeviceId = key.withoutPrefix("ed25519:") + val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) + if (potentialDevice != null && potentialDevice.isVerified) { + // Check signature validity? + try { + olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) + isMaterKeyTrusted = true + return@forEach + } catch (failure: Throwable) { + // log + Timber.w(failure, "Signature not valid?") + } + } + } + } + + if (!isMaterKeyTrusted) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + val myUserKey = myCrossSigningInfo.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + return UserTrustResult.KeyNotSigned(myUserKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) + } + + val mySSKey = myCrossSigningInfo.selfSigningKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + return UserTrustResult.KeyNotSigned(mySSKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) + } + + return UserTrustResult.Success + } + + override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return cryptoStore.getCrossSigningInfo(otherUserId) + } + + override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> { + return cryptoStore.getLiveCrossSigningInfo(userId) + } + + override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return cryptoStore.getMyCrossSigningInfo() + } + + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return cryptoStore.getCrossSigningPrivateKeys() + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> { + return cryptoStore.getLiveCrossSigningPrivateKeys() + } + + override fun canCrossSign(): Boolean { + return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null + } + + override fun allPrivateKeysKnown(): Boolean { + return checkSelfTrust().isVerified() + && cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() + } + + override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - Mark user $userId as trusted ") + // We should have this user keys + val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() + if (otherMasterKeys == null) { + callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) + return@launch + } + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + return@launch + } + val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey + if (userPubKey == null || userPkSigning == null) { + callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) + return@launch + } + + // Sign the other MasterKey with our UserSigning key + val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, + otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("## CrossSigning - Failed to sign")) + return@launch + } + + cryptoStore.setUserKeysAsTrusted(otherUserId, true) + // TODO update local copy with new signature directly here? kind of local echo of trust? + + Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + val uploadQuery = UploadSignatureQueryBuilder() + .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) + } + } + + override fun markMyMasterKeyAsTrusted() { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + checkSelfTrust() + } + } + + override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // This device should be yours + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device == null) { + callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) + return@launch + } + + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("CrossSigning is not setup for this account")) + return@launch + } + + val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey + if (ssPubKey == null || selfSigningPkSigning == null) { + callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) + return@launch + } + + // Sign with self signing + val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("Failed to sign")) + return@launch + } + val toUpload = device.copy( + signatures = mapOf( + userId + to + mapOf( + "ed25519:$ssPubKey" to newSignature + ) + ) + ) + + val uploadQuery = UploadSignatureQueryBuilder() + .withDeviceInfo(toUpload) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) + } + } + + override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) + ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) + + val myKeys = getUserCrossSigningKeys(userId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + + if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) + + val otherKeys = getUserCrossSigningKeys(otherUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId)) + + // TODO should we force verification ? + if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) + + // Check if the trust chain is valid + /* + * â”â”â”â”â”â”â”â”â”┓ â”â”â”â”â”â”â”â”â”┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * â”—â”â”â”â”â”â”â”â”â”› â”—â”â”â”â”â”â”â”â”â”› + * MSK ┌────────────▶MSK + * │ + * │ │ │ + * │ SSK │ └──▶ SSK ──────────────────┠+ * │ │ │ + * │ │ USK │ + * └──▶ USK ────────────┘ (not visible by │ + * Alice) │ + * â–¼ + * ┌──────────────┠+ * │ BOB's Device │ + * └──────────────┘ + */ + + val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") + ?: return legacyFallbackTrust( + locallyTrusted, + DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey() + ?.unpaddedBase64PublicKey + ?: "" + ) + ) + + // Check bob's device is signed by bob's SSK + try { + olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + } catch (e: Throwable) { + return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) + } + + return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) + } + + private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { + return if (locallyTrusted == true) { + DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) + } else { + crossSignTrustFail + } + } + + override fun onUsersDeviceUpdate(userIds: List<String>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") + userIds.forEach { otherUserId -> + checkUserTrust(otherUserId).let { + Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") + setUserKeysAsTrusted(otherUserId, it.isVerified()) + } + } + } + + // now check device trust + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + userIds.forEach { otherUserId -> + // TODO if my keys have changes, i should recheck all devices of all users? + val devices = cryptoStore.getUserDeviceList(otherUserId) + devices?.forEach { device -> + val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + + if (otherUserId == userId) { + // It's me, i should check if a newly trusted device is signing my master key + // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust + setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) + } + } + + eventBus.post(CryptoToSessionUserTrustChange(userIds)) + } + } + + private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { + val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() + cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) + // If it's me, recheck trust of all users and devices? + val users = ArrayList<String>() + if (otherUserId == userId && currentTrust != trusted) { +// reRequestAllPendingRoomKeyRequest() + cryptoStore.updateUsersTrust { + users.add(it) + checkUserTrust(it).isVerified() + } + + users.forEach { + cryptoStore.getUserDeviceList(it)?.forEach { device -> + val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + } + } + } + +// private fun reRequestAllPendingRoomKeyRequest() { +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") +// cryptoStore.getOutgoingRoomKeyRequests().forEach { +// it.requestBody?.let { requestBody -> +// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { +// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) +// } else { +// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) +// } +// } +// } +// } +// } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c371c84adecb69cfdb099e1710b81ef19567e034 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +data class DeviceTrustLevel( + val crossSigningVerified: Boolean, + val locallyVerified: Boolean? +) { + fun isVerified() = crossSigningVerified || locallyVerified == true + fun isCrossSigningVerified() = crossSigningVerified + fun isLocallyVerified() = locallyVerified +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..cabfae174849482938f205b902f4ecd7560e0917 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo + +sealed class DeviceTrustResult { + data class Success(val level: DeviceTrustLevel) : DeviceTrustResult() + data class UnknownDevice(val deviceID: String) : DeviceTrustResult() + data class CrossSigningNotConfigured(val userID: String) : DeviceTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : DeviceTrustResult() + data class MissingDeviceSignature(val deviceId: String, val signingKey: String) : DeviceTrustResult() + data class InvalidDeviceSignature(val deviceId: String, val signingKey: String, val throwable: Throwable?) : DeviceTrustResult() +} + +fun DeviceTrustResult.isSuccess() = this is DeviceTrustResult.Success +fun DeviceTrustResult.isCrossSignedVerified() = this is DeviceTrustResult.Success && level.isCrossSigningVerified() +fun DeviceTrustResult.isLocallyVerified() = this is DeviceTrustResult.Success && level.isLocallyVerified() == true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8178a8810db2c34c102dd95736658f2912b8c9c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import android.util.Base64 +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import timber.log.Timber + +fun CryptoDeviceInfo.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun CryptoCrossSigningKey.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun ByteArray.toBase64NoPadding(): String { + return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) +} + +fun String.fromBase64(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) +} + +/** + * Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source + */ +fun String.fromBase64Safe(): ByteArray? { + return try { + Base64.decode(this, Base64.DEFAULT) + } catch (throwable: Throwable) { + Timber.e(throwable, "Unable to decode base64 string") + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b06d79693549ea5cdba52d35b3304b2a4ec70b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +data class SessionToCryptoRoomMembersUpdate( + val roomId: String, + val isDirect: Boolean, + val userIds: List<String> +) + +data class CryptoToSessionUserTrustChange( + val userIds: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt new file mode 100644 index 0000000000000000000000000000000000000000..e8c131760408e53d75e2fc54a1217a1982e69bb2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject + +internal class ShieldTrustUpdater @Inject constructor( + private val eventBus: EventBus, + private val computeTrustTask: ComputeTrustTask, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, + private val roomSummaryUpdater: RoomSummaryUpdater +): SessionLifecycleObserver { + + companion object { + private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") + private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher() + } + + private val backgroundSessionRealm = AtomicReference<Realm>() + + private val isStarted = AtomicBoolean() + + override fun onStart() { + if (isStarted.compareAndSet(false, true)) { + eventBus.register(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) + } + } + } + + override fun onStop() { + if (isStarted.compareAndSet(true, false)) { + eventBus.unregister(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.getAndSet(null).also { + it?.close() + } + } + } + } + + @Subscribe + fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) { + if (!isStarted.get()) { + return + } + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect)) + // We need to send that back to session base + backgroundSessionRealm.get()?.executeTransaction { realm -> + roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) + } + } + } + + @Subscribe + fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { + if (!isStarted.get()) { + return + } + onCryptoDevicesChange(update.userIds) + } + + private fun onCryptoDevicesChange(users: List<String>) { + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val realm = backgroundSessionRealm.get() ?: return@launch + val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) + .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) + .distinct(RoomMemberSummaryEntityFields.ROOM_ID) + .findAll() + .map { it.roomId } + + distinctRoomIds.forEach { roomId -> + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummary?.isEncrypted.orFalse()) { + val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + try { + val updatedTrust = computeTrustTask.execute( + ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true) + ) + realm.executeTransaction { + roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust) + } + } catch (failure: Throwable) { + Timber.e(failure) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..878cbd0b32ca4f6029473eb4664b5b5f6b2adeb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey + +sealed class UserTrustResult { + object Success : UserTrustResult() + + // data class Success(val deviceID: String, val crossSigned: Boolean) : UserTrustResult() + // data class UnknownDevice(val deviceID: String) : UserTrustResult() + data class CrossSigningNotConfigured(val userID: String) : UserTrustResult() + + data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult() + data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult() + data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult() +} + +fun UserTrustResult.isVerified() = this is UserTrustResult.Success diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt new file mode 100644 index 0000000000000000000000000000000000000000..949677182c72bc4c528534348fbe454f8c29fd5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -0,0 +1,1454 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import android.os.Looper +import androidx.annotation.UiThread +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.ObjectSigner +import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.awaitCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.olm.OlmException +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkMessage +import timber.log.Timber +import java.security.InvalidParameterException +import javax.inject.Inject +import kotlin.random.Random + +/** + * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +@SessionScope +internal class DefaultKeysBackupService @Inject constructor( + @UserId private val userId: String, + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val objectSigner: ObjectSigner, + // Actions + private val megolmSessionDataImporter: MegolmSessionDataImporter, + // Tasks + private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, + private val deleteBackupTask: DeleteBackupTask, + private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask, + private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, + private val deleteSessionDataTask: DeleteSessionsDataTask, + private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, + private val getKeysBackupVersionTask: GetKeysBackupVersionTask, + private val getRoomSessionDataTask: GetRoomSessionDataTask, + private val getRoomSessionsDataTask: GetRoomSessionsDataTask, + private val getSessionsDataTask: GetSessionsDataTask, + private val storeRoomSessionDataTask: StoreRoomSessionDataTask, + private val storeSessionsDataTask: StoreRoomSessionsDataTask, + private val storeSessionDataTask: StoreSessionsDataTask, + private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, + // Task executor + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : KeysBackupService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val keysBackupStateManager = KeysBackupStateManager(uiHandler) + + // The backup version + override var keysBackupVersion: KeysVersionResult? = null + private set + + // The backup key being used. + private var backupOlmPkEncryption: OlmPkEncryption? = null + + private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null + + private var keysBackupStateListener: KeysBackupStateListener? = null + + override val isEnabled: Boolean + get() = keysBackupStateManager.isEnabled + + override val isStucked: Boolean + get() = keysBackupStateManager.isStucked + + override val state: KeysBackupState + get() = keysBackupStateManager.state + + override val currentBackupVersion: String? + get() = keysBackupVersion?.version + + override fun addListener(listener: KeysBackupStateListener) { + keysBackupStateManager.addListener(listener) + } + + override fun removeListener(listener: KeysBackupStateListener) { + keysBackupStateManager.removeListener(listener) + } + + override fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback<MegolmBackupCreationInfo>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + withContext(coroutineDispatchers.crypto) { + val olmPkDecryption = OlmPkDecryption() + val megolmBackupAuthData = if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") + } + } + } + } + } + + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + MegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations + ) + } else { + val publicKey = olmPkDecryption.generateKey() + + MegolmBackupAuthData( + publicKey = publicKey + ) + } + + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary()) + + val signedMegolmBackupAuthData = megolmBackupAuthData.copy( + signatures = objectSigner.signObject(canonicalJson) + ) + + MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) + } + }.foldToCallback(callback) + } + } + + override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback<KeysVersion>) { + @Suppress("UNCHECKED_CAST") + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) + + keysBackupStateManager.state = KeysBackupState.Enabling + + createKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) { + this.callback = object : MatrixCallback<KeysVersion> { + override fun onSuccess(data: KeysVersion) { + // Reset backup markers. + cryptoStore.resetBackupMarkers() + + val keyBackupVersion = KeysVersionResult( + algorithm = createKeysBackupVersionBody.algorithm, + authData = createKeysBackupVersionBody.authData, + version = data.version, + // We can consider that the server does not have keys yet + count = 0, + hash = null + ) + + enableKeysBackup(keyBackupVersion) + + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + keysBackupStateManager.state = KeysBackupState.Disabled + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.crypto) { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (keysBackupVersion != null && version == keysBackupVersion!!.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } + + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback<Unit> { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } + } + } + } + .executeBy(taskExecutor) + } + } + } + + override fun canRestoreKeys(): Boolean { + // Server contains more keys than locally + val totalNumberOfKeysLocally = getTotalNumbersOfKeys() + + val keysBackupData = cryptoStore.getKeysBackupData() + + val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1 + // Not used for the moment + // val hashServer = keysBackupData?.backupLastServerHash + + return when { + totalNumberOfKeysLocally < totalNumberOfKeysServer -> { + // Server contains more keys than this device + true + } + totalNumberOfKeysLocally == totalNumberOfKeysServer -> { + // Same number, compare hash? + // TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment + false + } + else -> false + } + } + + override fun getTotalNumbersOfKeys(): Int { + return cryptoStore.inboundGroupSessionsCount(false) + } + + override fun getTotalNumbersOfBackedUpKeys(): Int { + return cryptoStore.inboundGroupSessionsCount(true) + } + + override fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback<Unit>?) { + // Get a status right now + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Reset previous listeners if any + resetBackupAllGroupSessionsListeners() + Timber.v("backupAllGroupSessions: backupProgress: $progress/$total") + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure") + } + + if (progress == total) { + Timber.v("backupAllGroupSessions: complete") + callback?.onSuccess(Unit) + return + } + + backupAllGroupSessionsCallback = callback + + // Listen to `state` change to determine when to call onBackupProgress and onComplete + keysBackupStateListener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure 2") + } + + // If backup is finished, notify the main listener + if (state === KeysBackupState.ReadyToBackUp) { + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + } + } + }) + } + }.also { keysBackupStateManager.addListener(it) } + + backupKeys() + } + }) + } + + override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback<KeysBackupVersionTrust>) { + // TODO Validate with François that this is correct + object : Task<KeysVersionResult, KeysBackupVersionTrust> { + override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust { + return getKeysBackupTrustBg(params) + } + } + .configureWith(keysBackupVersion) { + this.callback = callback + this.executionThread = TaskThread.COMPUTATION + } + .executeBy(taskExecutor) + } + + /** + * Check trust on a key backup version. + * This has to be called on background thread. + * + * @param keysBackupVersion the backup version to check. + * @return a KeysBackupVersionTrust object + */ + @WorkerThread + private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { + val keysBackupVersionTrust = KeysBackupVersionTrust() + val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() + + if (keysBackupVersion.algorithm == null + || authData == null + || authData.publicKey.isEmpty() + || authData.signatures.isNullOrEmpty()) { + Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") + return keysBackupVersionTrust + } + + val mySigs = authData.signatures[userId] + if (mySigs.isNullOrEmpty()) { + Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") + return keysBackupVersionTrust + } + + for ((keyId, mySignature) in mySigs) { + // XXX: is this how we're supposed to get the device id? + var deviceId: String? = null + val components = keyId.split(":") + if (components.size == 2) { + deviceId = components[1] + } + + if (deviceId != null) { + val device = cryptoStore.getUserDevice(userId, deviceId) + var isSignatureValid = false + + if (device == null) { + Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId") + } else { + val fingerprint = device.fingerprint() + if (fingerprint != null) { + try { + olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) + isSignatureValid = true + } catch (e: OlmException) { + Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") + } + } + + if (isSignatureValid && device.isVerified) { + keysBackupVersionTrust.usable = true + } + } + + val signature = KeysBackupVersionTrustSignature() + signature.device = device + signature.valid = isSignatureValid + signature.deviceId = deviceId + keysBackupVersionTrust.signatures.add(signature) + } + } + + return keysBackupVersionTrust + } + + override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback<Unit>) { + Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") + + // Get auth data to update it + val authData = getMegolmBackupAuthData(keysBackupVersion) + + if (authData == null) { + Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") + + callback.onFailure(IllegalArgumentException("Missing element")) + } else { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { + // Get current signatures, or create an empty set + val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap() + + if (trust) { + // Add current device signature + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) + + val deviceSignatures = objectSigner.signObject(canonicalJson) + + deviceSignatures[userId]?.forEach { entry -> + myUserSignatures[entry.key] = entry.value + } + } else { + // Remove current device signature + myUserSignatures.remove("ed25519:${credentials.deviceId}") + } + + // Create an updated version of KeysVersionResult + val newMegolmBackupAuthData = authData.copy() + + val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() + newSignatures[userId] = myUserSignatures + + val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( + signatures = newSignatures + ) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + @Suppress("UNCHECKED_CAST") + UpdateKeysBackupVersionBody( + algorithm = keysBackupVersion.algorithm, + authData = adapter.fromJson(newMegolmBackupAuthDataWithNewSignature.toJsonString()) as Map<String, Any>?, + version = keysBackupVersion.version!!) + } + + // And send it to the homeserver + updateKeysBackupVersionTask + .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + // Relaunch the state machine on this updated backup version + val newKeysBackupVersion = KeysVersionResult( + algorithm = keysBackupVersion.algorithm, + authData = updateKeysBackupVersionBody.authData, + version = keysBackupVersion.version, + hash = keysBackupVersion.hash, + count = keysBackupVersion.count + ) + + checkAndStartWithKeysBackupVersion(newKeysBackupVersion) + + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + } + } + + override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback<Unit>) { + Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val isValid = withContext(coroutineDispatchers.crypto) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) + } + + if (!isValid) { + Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") + + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } else { + trustKeysBackupVersion(keysBackupVersion, true, callback) + } + } + } + + override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback<Unit>) { + Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val recoveryKey = withContext(coroutineDispatchers.crypto) { + recoveryKeyFromPassword(password, keysBackupVersion, null) + } + + if (recoveryKey == null) { + Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") + + callback.onFailure(IllegalArgumentException("Missing element")) + } else { + // Check trust using the recovery key + trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) + } + } + } + + override fun onSecretKeyGossip(secret: String) { + Timber.i("## CrossSigning - onSecretKeyGossip") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + try { + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback<Unit> { + trustKeysBackupVersion(keysBackupVersion, true, it) + } + val importResult = awaitCallback<ImportRoomKeysResult> { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) + } + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + } + } catch (failure: Throwable) { + Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") + } + } + } + + /** + * Get public key from a Recovery key + * + * @param recoveryKey the recovery key + * @return the corresponding public key, from Olm + */ + @WorkerThread + private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + if (privateKey == null) { + Timber.w("pkPublicKeyFromRecoveryKey: private key is null") + + return null + } + + // Built the PK decryption with it + val pkPublicKey: String + + try { + val decryption = OlmPkDecryption() + pkPublicKey = decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + return null + } + + return pkPublicKey + } + + private fun resetBackupAllGroupSessionsListeners() { + backupAllGroupSessionsCallback = null + + keysBackupStateListener?.let { + keysBackupStateManager.removeListener(it) + } + + keysBackupStateListener = null + } + + override fun getBackupProgress(progressListener: ProgressListener) { + val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) + val total = cryptoStore.inboundGroupSessionsCount(false) + + progressListener.onProgress(backedUpKeys, total) + } + + override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback<ImportRoomKeysResult>) { + Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + val decryption = withContext(coroutineDispatchers.crypto) { + // Check if the recovery is valid before going any further + if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") + throw InvalidParameterException("Invalid recovery key") + } + + // Get a PK decryption instance + pkDecryptionFromRecoveryKey(recoveryKey) + } + if (decryption == null) { + // This should not happen anymore + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") + throw InvalidParameterException("Invalid recovery key") + } + + stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) + + // Get backed up keys from the homeserver + val data = getKeys(sessionId, roomId, keysVersionResult.version!!) + + withContext(coroutineDispatchers.crypto) { + val sessionsData = ArrayList<MegolmSessionData>() + // Restore that data + var sessionsFromHsCount = 0 + for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) { + for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) { + sessionsFromHsCount++ + + val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) + + sessionData?.let { + sessionsData.add(it) + } + } + } + Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + + " of $sessionsFromHsCount from the backup store on the homeserver") + + // Do not trigger a backup for them if they come from the backup version we are using + val backUp = keysVersionResult.version != keysBackupVersion?.version + if (backUp) { + Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" + + " to backup version: ${keysBackupVersion?.version}") + } + + // Import them into the crypto store + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Note: no need to post to UI thread, importMegolmSessionsData() will do it + stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) + } + } + } else { + null + } + + val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener) + + // Do not back up the key if it comes from a backup recovery + if (backUp) { + maybeBackupKeys() + } + // Save for next time and for gossiping + saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) + result + } + }.foldToCallback(callback) + } + } + + override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback<ImportRoomKeysResult>) { + Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) + } + } + } + } else { + null + } + + val recoveryKey = withContext(coroutineDispatchers.crypto) { + recoveryKeyFromPassword(password, keysBackupVersion, progressListener) + } + if (recoveryKey == null) { + Timber.v("backupKeys: Invalid configuration") + throw IllegalStateException("Invalid configuration") + } else { + awaitCallback<ImportRoomKeysResult> { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) + } + } + }.foldToCallback(callback) + } + } + + /** + * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable + * parameters and always returns a KeysBackupData object through the Callback + */ + private suspend fun getKeys(sessionId: String?, + roomId: String?, + version: String): KeysBackupData { + return if (roomId != null && sessionId != null) { + // Get key for the room and for the session + val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf( + roomId to RoomKeysBackupData(mutableMapOf( + sessionId to data + )) + )) + } else if (roomId != null) { + // Get all keys for the room + val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf(roomId to data)) + } else { + // Get all keys + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } + } + + @VisibleForTesting + @WorkerThread + fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + // Built the PK decryption with it + var decryption: OlmPkDecryption? = null + if (privateKey != null) { + try { + decryption = OlmPkDecryption() + decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + + return decryption + } + + /** + * Do a backup if there are new keys, with a delay + */ + fun maybeBackupKeys() { + when { + isStucked -> { + // If not already done, or in error case, check for a valid backup version on the homeserver. + // If there is one, maybeBackupKeys will be called again. + checkAndStartKeysBackup() + } + state == KeysBackupState.ReadyToBackUp -> { + keysBackupStateManager.state = KeysBackupState.WillBackUp + + // Wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) + + cryptoCoroutineScope.launch { + delay(delayInMs) + uiHandler.post { backupKeys() } + } + } + else -> { + Timber.v("maybeBackupKeys: Skip it because state: $state") + } + } + } + + override fun getVersion(version: String, + callback: MatrixCallback<KeysVersionResult?>) { + getKeysBackupVersionTask + .configureWith(version) { + this.callback = object : MatrixCallback<KeysVersionResult> { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.M_NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + } + } + .executeBy(taskExecutor) + } + + override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) { + getKeysBackupLastVersionTask + .configureWith { + this.callback = object : MatrixCallback<KeysVersionResult> { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.M_NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + } + } + .executeBy(taskExecutor) + } + + override fun forceUsingLastVersion(callback: MatrixCallback<Boolean>) { + getCurrentVersion(object : MatrixCallback<KeysVersionResult?> { + override fun onSuccess(data: KeysVersionResult?) { + val localBackupVersion = keysBackupVersion?.version + val serverBackupVersion = data?.version + + if (serverBackupVersion == null) { + if (localBackupVersion == null) { + // No backup on the server, and backup is not active + callback.onSuccess(true) + } else { + // No backup on the server, and we are currently backing up, so stop backing up + callback.onSuccess(false) + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + if (localBackupVersion == null) { + // backup on the server, and backup is not active + callback.onSuccess(false) + // Do a check + checkAndStartWithKeysBackupVersion(data) + } else { + // Backup on the server, and we are currently backing up, compare version + if (localBackupVersion == serverBackupVersion) { + // We are already using the last version of the backup + callback.onSuccess(true) + } else { + // We are not using the last version, so delete the current version we are using on the server + callback.onSuccess(false) + + // This will automatically check for the last version then + deleteBackup(localBackupVersion, null) + } + } + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } + + override fun checkAndStartKeysBackup() { + if (!isStucked) { + // Try to start or restart the backup only if it is in unknown or bad state + Timber.w("checkAndStartKeysBackup: invalid state: $state") + + return + } + + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver + + getCurrentVersion(object : MatrixCallback<KeysVersionResult?> { + override fun onSuccess(data: KeysVersionResult?) { + checkAndStartWithKeysBackupVersion(data) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + keysBackupStateManager.state = KeysBackupState.Unknown + } + }) + } + + private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { + Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") + + keysBackupVersion = keyBackupVersion + + if (keyBackupVersion == null) { + Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") + resetKeysBackupData() + keysBackupStateManager.state = KeysBackupState.Disabled + } else { + getKeysBackupTrust(keyBackupVersion, object : MatrixCallback<KeysBackupVersionTrust> { + override fun onSuccess(data: KeysBackupVersionTrust) { + val versionInStore = cryptoStore.getKeyBackupVersion() + + if (data.usable) { + Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") + // Check the version we used at the previous app run + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Timber.v(" -> clean the previously used version $versionInStore") + resetKeysBackupData() + } + + Timber.v(" -> enabling key backups") + enableKeysBackup(keyBackupVersion) + } else { + Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") + if (versionInStore != null) { + Timber.v(" -> disabling key backup") + resetKeysBackupData() + } + + keysBackupStateManager.state = KeysBackupState.NotTrusted + } + } + + override fun onFailure(failure: Throwable) { + // Cannot happen + } + }) + } + } + +/* ========================================================================================== + * Private + * ========================================================================================== */ + + /** + * Extract MegolmBackupAuthData data from a backup version. + * + * @param keysBackupData the key backup data + * + * @return the authentication if found and valid, null in other case + */ + private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { + if (keysBackupData.version.isNullOrBlank() + || keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + || keysBackupData.authData == null) { + return null + } + + val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData() + + if (authData?.signatures == null || authData.publicKey.isBlank()) { + return null + } + + return authData + } + + /** + * Compute the recovery key from a password and key backup version. + * + * @param password the password. + * @param keysBackupData the backup and its auth data. + * + * @return the recovery key if successful, null in other cases + */ + @WorkerThread + private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? { + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("recoveryKeyFromPassword: invalid parameter") + return null + } + + if (authData.privateKeySalt.isNullOrBlank() + || authData.privateKeyIterations == null) { + Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") + + return null + } + + // Extract the recovery key from the passphrase + val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener) + + return computeRecoveryKey(data) + } + + /** + * Check if a recovery key matches key backup authentication data. + * + * @param recoveryKey the recovery key to challenge. + * @param keysBackupData the backup and its auth data. + * + * @return true if successful. + */ + @WorkerThread + private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean { + // Build PK decryption instance with the recovery key + val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey) + + if (publicKey == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null") + + return false + } + + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") + + return false + } + + // Compare both + if (publicKey != authData.publicKey) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") + + return false + } + + // Public keys match! + return true + } + + override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) { + val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let { + callback.onSuccess(it) + } + } + } + + /** + * Enable backing up of keys. + * This method will update the state and will start sending keys in nominal case + * + * @param keysVersionResult backup information object as returned by [getCurrentVersion]. + */ + private fun enableKeysBackup(keysVersionResult: KeysVersionResult) { + if (keysVersionResult.authData != null) { + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + if (retrievedMegolmBackupAuthData != null) { + keysBackupVersion = keysVersionResult + cryptoStore.setKeyBackupVersion(keysVersionResult.version) + + onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) + + try { + backupOlmPkEncryption = OlmPkEncryption().apply { + setRecipientKey(retrievedMegolmBackupAuthData.publicKey) + } + } catch (e: OlmException) { + Timber.e(e, "OlmException") + keysBackupStateManager.state = KeysBackupState.Disabled + return + } + + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + maybeBackupKeys() + } else { + Timber.e("Invalid authentication data") + keysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + Timber.e("Invalid authentication data") + keysBackupStateManager.state = KeysBackupState.Disabled + } + } + + /** + * Update the DB with data fetch from the server + */ + private fun onServerDataRetrieved(count: Int?, hash: String?) { + cryptoStore.setKeysBackupData(KeysBackupDataEntity() + .apply { + backupLastServerNumberOfKeys = count + backupLastServerHash = hash + } + ) + } + + /** + * Reset all local key backup data. + * + * Note: This method does not update the state + */ + private fun resetKeysBackupData() { + resetBackupAllGroupSessionsListeners() + + cryptoStore.setKeyBackupVersion(null) + cryptoStore.setKeysBackupData(null) + backupOlmPkEncryption = null + + // Reset backup markers + cryptoStore.resetBackupMarkers() + } + + /** + * Send a chunk of keys to backup + */ + @UiThread + private fun backupKeys() { + Timber.v("backupKeys") + + // Sanity check, as this method can be called after a delay, the state may have change during the delay + if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) { + Timber.v("backupKeys: Invalid configuration") + backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) + resetBackupAllGroupSessionsListeners() + + return + } + + if (state === KeysBackupState.BackingUp) { + // Do nothing if we are already backing up + Timber.v("backupKeys: Invalid state: $state") + return + } + + // Get a chunk of keys to backup + val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) + + Timber.v("backupKeys: 1 - ${olmInboundGroupSessionWrappers.size} sessions to back up") + + if (olmInboundGroupSessionWrappers.isEmpty()) { + // Backup is up to date + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + return + } + + keysBackupStateManager.state = KeysBackupState.BackingUp + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.crypto) { + Timber.v("backupKeys: 2 - Encrypting keys") + + // Gather data to send to the homeserver + // roomId -> sessionId -> MXKeyBackupData + val keysBackupData = KeysBackupData( + roomIdToRoomKeysBackupData = HashMap() + ) + + for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) { + val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper) + if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) { + val roomKeysBackupData = RoomKeysBackupData( + sessionIdToKeyBackupData = HashMap() + ) + keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData + } + + try { + keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!! + .sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + + Timber.v("backupKeys: 4 - Sending request") + + val sendingRequestCallback = object : MatrixCallback<BackupKeysResult> { + override fun onSuccess(data: BackupKeysResult) { + uiHandler.post { + Timber.v("backupKeys: 5a - Request complete") + + // Mark keys as backed up + cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + + if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { + Timber.v("backupKeys: All keys have been backed up") + onServerDataRetrieved(data.count, data.hash) + + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } else { + Timber.v("backupKeys: Continue to back up keys") + keysBackupStateManager.state = KeysBackupState.WillBackUp + + backupKeys() + } + } + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError) { + uiHandler.post { + Timber.e(failure, "backupKeys: backupKeys failed.") + + when (failure.error.code) { + MatrixError.M_NOT_FOUND, + MatrixError.M_WRONG_ROOM_KEYS_VERSION -> { + // Backup has been deleted on the server, or we are not using the last backup version + keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + resetKeysBackupData() + keysBackupVersion = null + + // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver + checkAndStartKeysBackup() + } + else -> + // Come back to the ready state so that we will retry on the next received key + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } + } + } else { + uiHandler.post { + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + + Timber.e("backupKeys: backupKeys failed.") + + // Retry a bit later + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } + } + } + } + + // Make the request + storeSessionDataTask + .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) { + this.callback = sendingRequestCallback + } + .executeBy(taskExecutor) + } + } + } + + @VisibleForTesting + @WorkerThread + fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData { + // Gather information for each key + val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!) + + // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at + // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format + val sessionData = olmInboundGroupSessionWrapper.exportKeys() + val sessionBackupData = mapOf( + "algorithm" to sessionData!!.algorithm, + "sender_key" to sessionData.senderKey, + "sender_claimed_keys" to sessionData.senderClaimedKeys, + "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain + ?: ArrayList<Any>()), + "session_key" to sessionData.sessionKey) + + var encryptedSessionBackupData: OlmPkMessage? = null + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + try { + val json = adapter.toJson(sessionBackupData) + + encryptedSessionBackupData = backupOlmPkEncryption?.encrypt(json) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + // Build backup data for that key + return KeyBackupData( + firstMessageIndex = try { + olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex + } catch (e: OlmException) { + Timber.e(e, "OlmException") + 0L + }, + forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size, + isVerified = device?.isVerified == true, + + sessionData = mapOf( + "ciphertext" to encryptedSessionBackupData!!.mCipherText, + "mac" to encryptedSessionBackupData.mMac, + "ephemeral" to encryptedSessionBackupData.mEphemeralKey) + ) + } + + @VisibleForTesting + @WorkerThread + fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { + var sessionBackupData: MegolmSessionData? = null + + val jsonObject = keyBackupData.sessionData + + val ciphertext = jsonObject?.get("ciphertext")?.toString() + val mac = jsonObject?.get("mac")?.toString() + val ephemeralKey = jsonObject?.get("ephemeral")?.toString() + + if (ciphertext != null && mac != null && ephemeralKey != null) { + val encrypted = OlmPkMessage() + encrypted.mCipherText = ciphertext + encrypted.mMac = mac + encrypted.mEphemeralKey = ephemeralKey + + try { + val decrypted = decryption.decrypt(encrypted) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(MegolmSessionData::class.java) + + sessionBackupData = adapter.fromJson(decrypted) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + if (sessionBackupData != null) { + sessionBackupData = sessionBackupData.copy( + sessionId = sessionId, + roomId = roomId + ) + } + } + + return sessionBackupData + } + + /* ========================================================================================== + * For test only + * ========================================================================================== */ + + // Direct access for test only + @VisibleForTesting + val store + get() = cryptoStore + + @VisibleForTesting + fun createFakeKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback<KeysVersion>) { + @Suppress("UNCHECKED_CAST") + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) + + createKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return cryptoStore.getKeyBackupRecoveryKeyInfo() + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, version) + } + + companion object { + // Maximum delay in ms in {@link maybeBackupKeys} + private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L + + // Maximum number of keys to send at a time to the homeserver. + private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 + } + +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString() = "KeysBackup for $userId" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt new file mode 100644 index 0000000000000000000000000000000000000000..e796514cf4f7967ea0819b11343334abe5b4ec29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +/** + * Utility to compute a backup private key from a password and vice-versa. + */ +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.listeners.ProgressListener +import timber.log.Timber +import java.util.UUID +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.xor + +private const val SALT_LENGTH = 32 +private const val DEFAULT_ITERATION = 500_000 + +data class GeneratePrivateKeyResult( + // The private key + val privateKey: ByteArray, + // the salt used to generate the private key + val salt: String, + // number of key derivations done on the generated private key. + val iterations: Int) + +/** + * Compute a private key from a password. + * + * @param password the password to use. + * + * @return a {privateKey, salt, iterations} tuple. + */ +@WorkerThread +fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressListener?): GeneratePrivateKeyResult { + val salt = generateSalt() + val iterations = DEFAULT_ITERATION + val privateKey = deriveKey(password, salt, iterations, progressListener) + + return GeneratePrivateKeyResult(privateKey, salt, iterations) +} + +/** + * Retrieve a private key from {password, salt, iterations} + * + * @param password the password used to generated the private key. + * @param salt the salt. + * @param iterations number of key derivations. + * @param progressListener the progress listener + * + * @return a private key. + */ +@WorkerThread +fun retrievePrivateKeyWithPassword(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener? = null): ByteArray { + return deriveKey(password, salt, iterations, progressListener) +} + +/** + * Compute a private key by deriving a password and a salt strings. + * + * @param password the password. + * @param salt the salt. + * @param iterations number of derivations. + * @param progressListener a listener to follow progress. + * + * @return a private key. + */ +@WorkerThread +fun deriveKey(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener?): ByteArray { + // Note: copied and adapted from MXMegolmExportEncryption + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + + // dklen = 256 + // hlen = 512 + val prf = Mac.getInstance("HmacSHA512") + + prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) + + // 256 bits key length + val dk = ByteArray(32) + val uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen + prf.update(salt.toByteArray()) + val int32BE = byteArrayOf(0, 0, 0, 1) + prf.update(int32BE) + prf.doFinal(uc, 0) + + // copy to the key + System.arraycopy(uc, 0, dk, 0, dk.size) + + var lastProgress = -1 + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in dk.indices) { + dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] + } + + val progress = (index + 1) * 100 / iterations + if (progress != lastProgress) { + lastProgress = progress + progressListener?.onProgress(lastProgress, 100) + } + } + + Timber.v("KeysBackupPassword: deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") + + return dk +} + +/** + * Generate a 32 chars salt + */ +private fun generateSalt(): String { + val salt = buildString { + do { + append(UUID.randomUUID().toString()) + } while (length < SALT_LENGTH) + } + + return salt.substring(0, SALT_LENGTH) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..19a1f081770a2bb7bfdfa12d0dde8c34041e3ef0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import timber.log.Timber + +internal class KeysBackupStateManager(private val uiHandler: Handler) { + + private val listeners = ArrayList<KeysBackupStateListener>() + + // Backup state + var state = KeysBackupState.Unknown + set(newState) { + Timber.v("KeysBackup: setState: $field -> $newState") + + field = newState + + // Notify listeners about the state change, on the ui thread + uiHandler.post { + synchronized(listeners) { + listeners.forEach { + // Use newState because state may have already changed again + it.onStateChange(newState) + } + } + } + } + + val isEnabled: Boolean + get() = state == KeysBackupState.ReadyToBackUp + || state == KeysBackupState.WillBackUp + || state == KeysBackupState.BackingUp + + // True if unknown or bad state + val isStucked: Boolean + get() = state == KeysBackupState.Unknown + || state == KeysBackupState.Disabled + || state == KeysBackupState.WrongBackUpVersion + || state == KeysBackupState.NotTrusted + + fun addListener(listener: KeysBackupStateListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + fun removeListener(listener: KeysBackupStateListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..de59aa8ae7203ee559805f3790a468dc7caf7aeb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.api + +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ +internal interface RoomKeysApi { + + /* ========================================================================================== + * Backup versions management + * ========================================================================================== */ + + /** + * Create a new keys backup version. + * @param createKeysBackupVersionBody the body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") + fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call<KeysVersion> + + /** + * Get the key backup last version + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") + fun getKeysBackupLastVersion(): Call<KeysVersionResult> + + /** + * Get information about the given version. + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + * + * @param version version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun getKeysBackupVersion(@Path("version") version: String): Call<KeysVersionResult> + + /** + * Update information about the given version. + * @param version version + * @param updateKeysBackupVersionBody the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun updateKeysBackupVersion(@Path("version") version: String, + @Body keysBackupVersionBody: UpdateKeysBackupVersionBody): Call<Unit> + + /* ========================================================================================== + * Storing keys + * ========================================================================================== */ + + /** + * Store the key for the given session in the given room, using the given backup version. + * + * + * If the server already has a backup in the backup version for the given session and room, then it will + * keep the "better" one. To determine which one is "better", key backups are compared first by the is_verified + * flag (true is better than false), then by the first_message_index (a lower number is better), and finally by + * forwarded_count (a lower number is better). + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup + * @param keyBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun storeRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String, + @Body keyBackupData: KeyBackupData): Call<BackupKeysResult> + + /** + * Store several keys for the given room, using the given backup version. + * + * @param roomId the room id + * @param version the version of the backup + * @param roomKeysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun storeRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String, + @Body roomKeysBackupData: RoomKeysBackupData): Call<BackupKeysResult> + + /** + * Store several keys, using the given backup version. + * + * @param version the version of the backup + * @param keysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun storeSessionsData(@Query("version") version: String, + @Body keysBackupData: KeysBackupData): Call<BackupKeysResult> + + /* ========================================================================================== + * Retrieving keys + * ========================================================================================== */ + + /** + * Retrieve the key for the given session in the given room from the backup. + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun getRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call<KeyBackupData> + + /** + * Retrieve all the keys for the given room from the backup. + * + * @param roomId the room id + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun getRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call<RoomKeysBackupData> + + /** + * Retrieve all the keys from the backup. + * + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun getSessionsData(@Query("version") version: String): Call<KeysBackupData> + + /* ========================================================================================== + * Deleting keys + * ========================================================================================== */ + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun deleteRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call<Unit> + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun deleteRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call<Unit> + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun deleteSessionsData(@Query("version") version: String): Call<Unit> + + /* ========================================================================================== + * Deleting backup + * ========================================================================================== */ + + /** + * Deletes a backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun deleteBackup(@Path("version") version: String): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt new file mode 100644 index 0000000000000000000000000000000000000000..871874bc9aec8aa4363a8ab682fe22bee75fbb28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import com.squareup.moshi.JsonClass + +/** + * Data model for response to [KeysBackup.isKeyBackupTrusted()]. + */ +@JsonClass(generateAdapter = true) +data class KeyBackupVersionTrust( + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList<KeyBackupVersionTrustSignature> = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt new file mode 100644 index 0000000000000000000000000000000000000000..955bd5e531d4319f6c56f5f824e92d6ad6feab3d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo + +/** + * A signature in a the `KeyBackupVersionTrust` object. + */ +class KeyBackupVersionTrustSignature { + + /** + * The device that signed the backup version. + */ + var device: CryptoDeviceInfo? = null + + /** + *Flag to indicate the signature from this device is valid. + */ + var valid = false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7d23c42ddea42394faf9aaca91529bb35dce7ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +/** + * Data model for response to [KeysBackup.getKeysBackupTrust()]. + */ +data class KeysBackupVersionTrust( + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList<KeysBackupVersionTrustSignature> = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt new file mode 100644 index 0000000000000000000000000000000000000000..8382fff6f2542f649c79e130e6838a04fd45a1f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo + +/** + * A signature in a `KeysBackupVersionTrust` object. + */ +class KeysBackupVersionTrustSignature { + + /** + * The id of the device that signed the backup version. + */ + var deviceId: String? = null + + /** + * The device that signed the backup version. + * Can be null if the device is not known. + */ + var device: CryptoDeviceInfo? = null + + /** + * Flag to indicate the signature from this device is valid. + */ + var valid = false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa5629e6d9cc52f1f0dcbc572588db0c70ed0b3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case + * of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ +@JsonClass(generateAdapter = true) +data class MegolmBackupAuthData( + /** + * The curve25519 public key used to encrypt the backups. + */ + @Json(name = "public_key") + val publicKey: String = "", + + /** + * In case of a backup created from a password, the salt associated with the backup + * private key. + */ + @Json(name = "private_key_salt") + val privateKeySalt: String? = null, + + /** + * In case of a backup created from a password, the number of key derivations. + */ + @Json(name = "private_key_iterations") + val privateKeyIterations: Int? = null, + + /** + * Signatures of the public key. + * userId -> (deviceSignKeyId -> signature) + */ + @Json(name = "signatures") + val signatures: Map<String, Map<String, String>>? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .toJson(this) + } + + /** + * Same as the parent [MXJSONModel JSONDictionary] but return only + * data that must be signed. + */ + fun signalableJSONDictionary(): Map<String, Any> = HashMap<String, Any>().apply { + put("public_key", publicKey) + + privateKeySalt?.let { + put("private_key_salt", it) + } + privateKeyIterations?.let { + put("private_key_iterations", it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..25b191e5bd0916cca7359f192223aba77962630a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +/** + * Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user + */ +data class MegolmBackupCreationInfo( + /** + * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ + val algorithm: String = "", + + /** + * Authentication data. + */ + val authData: MegolmBackupAuthData? = null, + + /** + * The Base58 recovery key. + */ + val recoveryKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..4903372abdd96954dd73237017cca32b0ae9a3d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BackupKeysResult( + + // The hash value which is an opaque string representing stored keys in the backup + var hash: String? = null, + + // The number of keys stored in the backup. + var count: Int? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f493571d3b4b1fed353c208eb0061c99c1e6b08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class CreateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt new file mode 100644 index 0000000000000000000000000000000000000000..b03d51894cda01a507194812c470aa492f695e8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean + +/** + * Backup data for one key. + */ +@JsonClass(generateAdapter = true) +data class KeyBackupData( + /** + * Required. The index of the first message in the session that the key can decrypt. + */ + @Json(name = "first_message_index") + val firstMessageIndex: Long = 0, + + /** + * Required. The number of times this key has been forwarded. + */ + @Json(name = "forwarded_count") + val forwardedCount: Int = 0, + + /** + * Whether the device backing up the key has verified the device that the key is from. + * Force to boolean because of https://github.com/matrix-org/synapse/issues/6977 + */ + @ForceToBoolean + @Json(name = "is_verified") + val isVerified: Boolean = false, + + /** + * Algorithm-dependent data. + */ + @Json(name = "session_data") + val sessionData: Map<String, Any>? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt new file mode 100644 index 0000000000000000000000000000000000000000..99031ca458e52df07f7733f51db4cd1a87fd4497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * <pre> + * Example: + * + * { + * "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + * "auth_data": { + * "public_key": "abcdefg", + * "signatures": { + * "something": { + * "ed25519:something": "hijklmnop" + * } + * } + * } + * } + * </pre> + */ +interface KeysAlgorithmAndData { + + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + val algorithm: String? + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + val authData: JsonDict? + + /** + * Facility method to convert authData to a MegolmBackupAuthData object + */ + fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .fromJsonValue(authData) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt new file mode 100644 index 0000000000000000000000000000000000000000..34c5d1c531cfaa49957562a6ba68a92dc1f452c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys in several rooms. + */ +@JsonClass(generateAdapter = true) +data class KeysBackupData( + // the keys are the room IDs, and the values are RoomKeysBackupData + @Json(name = "rooms") + val roomIdToRoomKeysBackupData: MutableMap<String, RoomKeysBackupData> = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ca8df3131ad3d0ddc1fd76dbde734d57ac03fc6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +data class KeysVersion( + // the keys backup version + var version: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..fd5d926871a1671108a58ec3c7cc908683795584 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class KeysVersionResult( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + + // the backup version + @Json(name = "version") + val version: String? = null, + + // The hash value which is an opaque string representing stored keys in the backup + @Json(name = "hash") + val hash: String? = null, + + // The number of keys stored in the backup. + @Json(name = "count") + val count: Int? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt new file mode 100644 index 0000000000000000000000000000000000000000..7564e54fc08d9f936d6022e8d0ef0a54d99f26d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys within a room. + */ +@JsonClass(generateAdapter = true) +data class RoomKeysBackupData( + // the keys are the session IDs, and the values are KeyBackupData + @Json(name = "sessions") + val sessionIdToKeyBackupData: MutableMap<String, KeyBackupData> = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..6de374d380cfa681ef84dbf63ae9300be9f43fe7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class UpdateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + + // the backup version, mandatory + @Json(name = "version") + val version: String +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b11e9171686ae56ceba70c05bcb861210c796c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface CreateKeysBackupVersionTask : Task<CreateKeysBackupVersionBody, KeysVersion> + +internal class DefaultCreateKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : CreateKeysBackupVersionTask { + + override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { + return executeRequest(eventBus) { + apiCall = roomKeysApi.createKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..25417ef4feb1b0363ce0fa55c805f20b342fe85e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteBackupTask : Task<DeleteBackupTask.Params, Unit> { + data class Params( + val version: String + ) +} + +internal class DefaultDeleteBackupTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteBackupTask { + + override suspend fun execute(params: DeleteBackupTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteBackup(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..12042f64598c5dd1645e8502157a33b843797aa0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteRoomSessionDataTask : Task<DeleteRoomSessionDataTask.Params, Unit> { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + +internal class DefaultDeleteRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteRoomSessionDataTask { + + override suspend fun execute(params: DeleteRoomSessionDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..92e5153d412e509c11e126d08cc7c9bc211ccdb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteRoomSessionsDataTask : Task<DeleteRoomSessionsDataTask.Params, Unit> { + data class Params( + val roomId: String, + val version: String + ) +} + +internal class DefaultDeleteRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteRoomSessionsDataTask { + + override suspend fun execute(params: DeleteRoomSessionsDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..66e1fa0203e8be0c0d6b1e81b613bfcd419cc2bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteSessionsDataTask : Task<DeleteSessionsDataTask.Params, Unit> { + data class Params( + val version: String + ) +} + +internal class DefaultDeleteSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteSessionsDataTask { + + override suspend fun execute(params: DeleteSessionsDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteSessionsData(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..afd0e85f590752bd9b1f52ee00ca596d640d05e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeysBackupLastVersionTask : Task<Unit, KeysVersionResult> + +internal class DefaultGetKeysBackupLastVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupLastVersionTask { + + override suspend fun execute(params: Unit): KeysVersionResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getKeysBackupLastVersion() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b454a83b893780f2d5d6cf46073380459334893f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeysBackupVersionTask : Task<String, KeysVersionResult> + +internal class DefaultGetKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupVersionTask { + + override suspend fun execute(params: String): KeysVersionResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c5d3c3afa81cdbf97de2bc6f9920a0d613b1ee2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomSessionDataTask : Task<GetRoomSessionDataTask.Params, KeyBackupData> { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + +internal class DefaultGetRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetRoomSessionDataTask { + + override suspend fun execute(params: GetRoomSessionDataTask.Params): KeyBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8b49d49d4dde9a013be3a82f42341a2c24b2fbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomSessionsDataTask : Task<GetRoomSessionsDataTask.Params, RoomKeysBackupData> { + data class Params( + val roomId: String, + val version: String + ) +} + +internal class DefaultGetRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetRoomSessionsDataTask { + + override suspend fun execute(params: GetRoomSessionsDataTask.Params): RoomKeysBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0a05eaff9f5970f0fa43ccd5a869c9358ce8621 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetSessionsDataTask : Task<GetSessionsDataTask.Params, KeysBackupData> { + data class Params( + val version: String + ) +} + +internal class DefaultGetSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetSessionsDataTask { + + override suspend fun execute(params: GetSessionsDataTask.Params): KeysBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getSessionsData(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..31a464dc38853087844ceb96e9c5a04fcb485066 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreRoomSessionDataTask : Task<StoreRoomSessionDataTask.Params, BackupKeysResult> { + data class Params( + val roomId: String, + val sessionId: String, + val version: String, + val keyBackupData: KeyBackupData + ) +} + +internal class DefaultStoreRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreRoomSessionDataTask { + + override suspend fun execute(params: StoreRoomSessionDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeRoomSessionData( + params.roomId, + params.sessionId, + params.version, + params.keyBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..057198aaf9052ce75a2b6ce545d51f05192e1d7f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreRoomSessionsDataTask : Task<StoreRoomSessionsDataTask.Params, BackupKeysResult> { + data class Params( + val roomId: String, + val version: String, + val roomKeysBackupData: RoomKeysBackupData + ) +} + +internal class DefaultStoreRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreRoomSessionsDataTask { + + override suspend fun execute(params: StoreRoomSessionsDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeRoomSessionsData( + params.roomId, + params.version, + params.roomKeysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..33f6a0862d32b65a0f2ebe4b0fc4d2693ac10ce2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreSessionsDataTask : Task<StoreSessionsDataTask.Params, BackupKeysResult> { + data class Params( + val version: String, + val keysBackupData: KeysBackupData + ) +} + +internal class DefaultStoreSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreSessionsDataTask { + + override suspend fun execute(params: StoreSessionsDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeSessionsData( + params.version, + params.keysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..68725b1eb117d34732a6cd8579f85e9fd1c8b3e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdateKeysBackupVersionTask : Task<UpdateKeysBackupVersionTask.Params, Unit> { + data class Params( + val version: String, + val keysBackupVersionBody: UpdateKeysBackupVersionBody + ) +} + +internal class DefaultUpdateKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : UpdateKeysBackupVersionTask { + + override suspend fun execute(params: UpdateKeysBackupVersionTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt new file mode 100644 index 0000000000000000000000000000000000000000..adbcd18d121e67a8a8a4b068cad80c44077b4319 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import java.math.BigInteger + +/** + * Ref: https://github.com/bitcoin-labs/bitcoin-mobile-android/blob/master/src/bitcoinj/java/com/google/bitcoin/core/Base58.java + * + * + * A custom form of base58 is used to encode BitCoin addresses. Note that this is not the same base58 as used by + * Flickr, which you may see reference to around the internet. + * + * Satoshi says: why base-58 instead of standard base-64 encoding? + * + * * Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers. + * * A string with non-alphanumeric characters is not as easily accepted as an account number. + * * E-mail usually won't line-break if there's no punctuation to break at. + * * Doubleclicking selects the whole number as one word if it's all alphanumeric. + * + */ +private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +private val BASE = BigInteger.valueOf(58) + +/** + * Encode a byte array to a human readable string with base58 chars + */ +fun base58encode(input: ByteArray): String { + var bi = BigInteger(1, input) + val s = StringBuffer() + while (bi >= BASE) { + val mod = bi.mod(BASE) + s.insert(0, ALPHABET[mod.toInt()]) + bi = bi.subtract(mod).divide(BASE) + } + s.insert(0, ALPHABET[bi.toInt()]) + // Convert leading zeros too. + for (anInput in input) { + if (anInput.toInt() == 0) { + s.insert(0, ALPHABET[0]) + } else { + break + } + } + return s.toString() +} + +/** + * Decode a base58 String to a byte array + */ +fun base58decode(input: String): ByteArray { + var result = decodeToBigInteger(input).toByteArray() + + // Remove the first leading zero if any + if (result[0] == 0.toByte()) { + result = result.copyOfRange(1, result.size) + } + + return result +} + +private fun decodeToBigInteger(input: String): BigInteger { + var bi = BigInteger.valueOf(0) + // Work backwards through the string. + for (i in input.length - 1 downTo 0) { + val alphaIndex = ALPHABET.indexOf(input[i]) + bi = bi.add(BigInteger.valueOf(alphaIndex.toLong()).multiply(BASE.pow(input.length - 1 - i))) + } + return bi +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..78697ca9cef3fbafb0bc16830bafd329e3f872b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import kotlin.experimental.xor + +/** + * See https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ + +private const val CHAR_0 = 0x8B.toByte() +private const val CHAR_1 = 0x01.toByte() + +private const val RECOVERY_KEY_LENGTH = 2 + 32 + 1 + +/** + * Tell if the format of the recovery key is correct + * + * @param recoveryKey + * @return true if the format of the recovery key is correct + */ +fun isValidRecoveryKey(recoveryKey: String?): Boolean { + return extractCurveKeyFromRecoveryKey(recoveryKey) != null +} + +/** + * Compute recovery key from curve25519 key + * + * @param curve25519Key + * @return the recovery key + */ +fun computeRecoveryKey(curve25519Key: ByteArray): String { + // Append header and parity + val data = ByteArray(curve25519Key.size + 3) + + // Header + data[0] = CHAR_0 + data[1] = CHAR_1 + + // Copy key and compute parity + var parity: Byte = CHAR_0 xor CHAR_1 + + for (i in curve25519Key.indices) { + data[i + 2] = curve25519Key[i] + parity = parity xor curve25519Key[i] + } + + // Parity + data[curve25519Key.size + 2] = parity + + // Do not add white space every 4 chars, it's up to the presenter to do it + return base58encode(data) +} + +/** + * Please call [.isValidRecoveryKey] and ensure it returns true before calling this method + * + * @param recoveryKey the recovery key + * @return curveKey, or null in case of error + */ +fun extractCurveKeyFromRecoveryKey(recoveryKey: String?): ByteArray? { + if (recoveryKey == null) { + return null + } + + // Remove any space + val spaceFreeRecoveryKey = recoveryKey.replace("""\s""".toRegex(), "") + + val b58DecodedKey = base58decode(spaceFreeRecoveryKey) + + // Check length + if (b58DecodedKey.size != RECOVERY_KEY_LENGTH) { + return null + } + + // Check first byte + if (b58DecodedKey[0] != CHAR_0) { + return null + } + + // Check second byte + if (b58DecodedKey[1] != CHAR_1) { + return null + } + + // Check parity + var parity: Byte = 0 + + for (i in 0 until RECOVERY_KEY_LENGTH) { + parity = parity xor b58DecodedKey[i] + } + + if (parity != 0.toByte()) { + return null + } + + // Remove header and parity bytes + val result = ByteArray(b58DecodedKey.size - 3) + + for (i in 2 until b58DecodedKey.size - 1) { + result[i - 2] = b58DecodedKey[i] + } + + return result +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..168258acd2ce482f32ff98e0c2c0b7665d8ad285 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo + +data class CryptoCrossSigningKey( + override val userId: String, + + val usages: List<String>?, + + override val keys: Map<String, String>, + + override val signatures: Map<String, Map<String, String>>?, + + var trustLevel: DeviceTrustLevel? = null +) : CryptoInfo { + + override fun signalableJSONDictionary(): Map<String, Any> { + val map = HashMap<String, Any>() + userId.let { map["user_id"] = it } + usages?.let { map["usage"] = it } + keys.let { map["keys"] = it } + + return map + } + + val unpaddedBase64PublicKey: String? = keys.values.firstOrNull() + + val isMasterKey = usages?.contains(KeyUsage.MASTER.value) ?: false + val isSelfSigningKey = usages?.contains(KeyUsage.SELF_SIGNING.value) ?: false + val isUserKey = usages?.contains(KeyUsage.USER_SIGNING.value) ?: false + + fun addSignatureAndCopy(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + val updated = (signatures?.toMutableMap() ?: HashMap()) + val userMap = updated[userId]?.toMutableMap() + ?: HashMap<String, String>().also { updated[userId] = it } + userMap["ed25519:$signedWithNoPrefix"] = signature + + return this.copy( + signatures = updated + ) + } + + fun copyForSignature(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + return this.copy( + signatures = mapOf(userId to mapOf("ed25519:$signedWithNoPrefix" to signature)) + ) + } + + data class Builder( + val userId: String, + val usage: KeyUsage, + private var base64Pkey: String? = null, + private val signatures: ArrayList<Triple<String, String, String>> = ArrayList() + ) { + + fun key(publicKeyBase64: String) = apply { + base64Pkey = publicKeyBase64 + } + + fun signature(userId: String, keySignedBase64: String, base64Signature: String) = apply { + signatures.add(Triple(userId, keySignedBase64, base64Signature)) + } + + fun build(): CryptoCrossSigningKey { + val b64key = base64Pkey ?: throw IllegalArgumentException("") + + val signMap = HashMap<String, HashMap<String, String>>() + signatures.forEach { info -> + val uMap = signMap[info.first] + ?: HashMap<String, String>().also { signMap[info.first] = it } + uMap["ed25519:${info.second}"] = info.third + } + + return CryptoCrossSigningKey( + userId = userId, + usages = listOf(usage.value), + keys = mapOf("ed25519:$b64key" to b64key), + signatures = signMap) + } + } +} + +enum class KeyUsage(val value: String) { + MASTER("master"), + SELF_SIGNING("self_signing"), + USER_SIGNING("user_signing") +} + +internal fun CryptoCrossSigningKey.toRest(): RestKeyInfo { + return CryptoInfoMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..b4bba727180f3967e6a6e8a41f7e051691b172ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity + +data class CryptoDeviceInfo( + val deviceId: String, + override val userId: String, + var algorithms: List<String>? = null, + override val keys: Map<String, String>? = null, + override val signatures: Map<String, Map<String, String>>? = null, + val unsigned: UnsignedDeviceInfo? = null, + var trustLevel: DeviceTrustLevel? = null, + var isBlocked: Boolean = false, + val firstTimeSeenLocalTs: Long? = null +) : CryptoInfo { + + val isVerified: Boolean + get() = trustLevel?.isVerified() == true + + val isUnknown: Boolean + get() = trustLevel == null + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("ed25519:$deviceId") + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("curve25519:$deviceId") + } + + /** + * @return the display name + */ + fun displayName(): String? { + return unsigned?.deviceDisplayName + } + + override fun signalableJSONDictionary(): Map<String, Any> { + val map = HashMap<String, Any>() + map["device_id"] = deviceId + map["user_id"] = userId + algorithms?.let { map["algorithms"] = it } + keys?.let { map["keys"] = it } + return map + } +} + +internal fun CryptoDeviceInfo.toRest(): DeviceKeys { + return CryptoInfoMapper.map(this) +} + +internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity { + return CryptoMapper.mapToEntity(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..116205dce46a5405ff6743440e68a8c0c6985ca7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +/** + * Generic crypto info. + * Can be a device (CryptoDeviceInfo), as well as a CryptoCrossSigningInfo (can be seen as a kind of virtual device) + */ +interface CryptoInfo { + + val userId: String + + val keys: Map<String, String>? + + val signatures: Map<String, Map<String, String>>? + + fun signalableJSONDictionary(): Map<String, Any> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..ead1dd5457e4d27ccfa57f75b984467bf574cebb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo + +internal object CryptoInfoMapper { + + fun map(deviceKeysWithUnsigned: DeviceKeysWithUnsigned): CryptoDeviceInfo { + return CryptoDeviceInfo( + deviceId = deviceKeysWithUnsigned.deviceId, + userId = deviceKeysWithUnsigned.userId, + algorithms = deviceKeysWithUnsigned.algorithms, + keys = deviceKeysWithUnsigned.keys, + signatures = deviceKeysWithUnsigned.signatures, + unsigned = deviceKeysWithUnsigned.unsigned, + trustLevel = null + ) + } + + fun map(cryptoDeviceInfo: CryptoDeviceInfo): DeviceKeys { + return DeviceKeys( + deviceId = cryptoDeviceInfo.deviceId, + algorithms = cryptoDeviceInfo.algorithms, + keys = cryptoDeviceInfo.keys, + signatures = cryptoDeviceInfo.signatures, + userId = cryptoDeviceInfo.userId + ) + } + + fun map(keyInfo: RestKeyInfo): CryptoCrossSigningKey { + return CryptoCrossSigningKey( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys.orEmpty(), + signatures = keyInfo.signatures, + trustLevel = null + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): RestKeyInfo { + return RestKeyInfo( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys, + signatures = keyInfo.signatures + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ecda951df803dfb880bb0a77d023dfe94c77bad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +data class ImportRoomKeysResult(val totalNumberOfKeys: Int, + val successfullyNumberOfImportedKeys: Int) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt new file mode 100755 index 0000000000000000000000000000000000000000..1733cc39137705e7582e7a79e2c6bee9d01a5634 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class MXDeviceInfo( + /** + * The id of this device. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * the user id + */ + @Json(name = "user_id") + val userId: String, + + /** + * The list of algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List<String>? = null, + + /** + * A map from "<key type>:<deviceId>" to "<base64-encoded key>". + */ + @Json(name = "keys") + val keys: Map<String, String>? = null, + + /** + * The signature of this MXDeviceInfo. + * A map from "<userId>" to a map from "<key type>:<deviceId>" to "<signature>" + */ + @Json(name = "signatures") + val signatures: Map<String, Map<String, String>>? = null, + + /* + * Additional data from the home server. + */ + @Json(name = "unsigned") + val unsigned: JsonDict? = null, + + /** + * Verification state of this device. + */ + val verified: Int = DEVICE_VERIFICATION_UNKNOWN +) : Serializable { + /** + * Tells if the device is unknown + * + * @return true if the device is unknown + */ + val isUnknown: Boolean + get() = verified == DEVICE_VERIFICATION_UNKNOWN + + /** + * Tells if the device is verified. + * + * @return true if the device is verified + */ + val isVerified: Boolean + get() = verified == DEVICE_VERIFICATION_VERIFIED + + /** + * Tells if the device is unverified. + * + * @return true if the device is unverified + */ + val isUnverified: Boolean + get() = verified == DEVICE_VERIFICATION_UNVERIFIED + + /** + * Tells if the device is blocked. + * + * @return true if the device is blocked + */ + val isBlocked: Boolean + get() = verified == DEVICE_VERIFICATION_BLOCKED + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("ed25519:$deviceId") + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("curve25519:$deviceId") + } + + /** + * @return the display name + */ + fun displayName(): String? { + return unsigned?.get("device_display_name") as? String + } + + /** + * @return the signed data map + */ + fun signalableJSONDictionary(): Map<String, Any> { + val map = HashMap<String, Any>() + + map["device_id"] = deviceId + + map["user_id"] = userId + + if (null != algorithms) { + map["algorithms"] = algorithms + } + + if (null != keys) { + map["keys"] = keys + } + + return map + } + + /** + * @return a dictionary of the parameters + */ + fun toDeviceKeys(): DeviceKeys { + return DeviceKeys( + userId = userId, + deviceId = deviceId, + algorithms = algorithms!!, + keys = keys!!, + signatures = signatures!! + ) + } + + override fun toString(): String { + return "MXDeviceInfo $userId:$deviceId" + } + + companion object { + // This device is a new device and the user was not warned it has been added. + const val DEVICE_VERIFICATION_UNKNOWN = -1 + + // The user has not yet verified this device. + const val DEVICE_VERIFICATION_UNVERIFIED = 0 + + // The user has verified this device. + const val DEVICE_VERIFICATION_VERIFIED = 1 + + // The user has blocked this device. + const val DEVICE_VERIFICATION_BLOCKED = 2 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt new file mode 100755 index 0000000000000000000000000000000000000000..0f0b289bc62f09e0df70c23ac9db5e46f7b32678 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.session.events.model.Content + +data class MXEncryptEventContentResult( + /** + * The encrypted event content + */ + val eventContent: Content, + /** + * the event type + */ + val eventType: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt new file mode 100755 index 0000000000000000000000000000000000000000..8808c83985af433bfd6ee674eaa450fb825dbb4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.util.JsonDict +import timber.log.Timber + +data class MXKey( + /** + * The type of the key (in the example: "signed_curve25519"). + */ + val type: String, + + /** + * The id of the key (in the example: "AAAAFw"). + */ + private val keyId: String, + + /** + * The key (in the example: "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4"). + */ + val value: String, + + /** + * signature user Id to [deviceid][signature] + */ + private val signatures: Map<String, Map<String, String>> +) { + + /** + * @return the signed data map + */ + fun signalableJSONDictionary(): Map<String, Any> { + return mapOf("key" to value) + } + + /** + * Returns a signature for an user Id and a signkey + * + * @param userId the user id + * @param signkey the sign key + * @return the signature + */ + fun signatureForUserId(userId: String, signkey: String): String? { + // sanity checks + if (userId.isNotBlank() && signkey.isNotBlank()) { + return signatures[userId]?.get(signkey) + } + + return null + } + + companion object { + /** + * Key types. + */ + const val KEY_CURVE_25519_TYPE = "curve25519" + const val KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519" + // const val KEY_ED_25519_TYPE = "ed25519" + + /** + * Convert a map to a MXKey + * + * @param map the map to convert + * + * Json Example: + * + * <pre> + * "signed_curve25519:AAAAFw": { + * "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4", + * "signatures": { + * "@userId:matrix.org": { + * "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg" + * } + * } + * } + * </pre> + * + * into several val members + */ + fun from(map: Map<String, JsonDict>?): MXKey? { + if (map?.isNotEmpty() == true) { + val firstKey = map.keys.first() + + val components = firstKey.split(":").dropLastWhile { it.isEmpty() } + + if (components.size == 2) { + val params = map[firstKey] + if (params != null) { + if (params["key"] is String) { + @Suppress("UNCHECKED_CAST") + return MXKey( + type = components[0], + keyId = components[1], + value = params["key"] as String, + signatures = params["signatures"] as Map<String, Map<String, String>> + ) + } + } + } + } + + // Error case + Timber.e("## Unable to parse map") + return null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt new file mode 100755 index 0000000000000000000000000000000000000000..30f4a6bba1bfb9943d5e5664b4e65738e793d25a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import java.io.Serializable + +data class MXOlmSessionResult( + /** + * the device + */ + val deviceInfo: CryptoDeviceInfo, + /** + * Base64 olm session id. + * null if no session could be established. + */ + var sessionId: String?) : Serializable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt new file mode 100755 index 0000000000000000000000000000000000000000..598f16bdf39956cb5a72df213a4e729ae6bf0b83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Content + +class MXQueuedEncryption { + + /** + * The data to encrypt. + */ + var eventContent: Content? = null + var eventType: String? = null + + /** + * the asynchronous callback + */ + var apiCallback: MatrixCallback<Content>? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt new file mode 100755 index 0000000000000000000000000000000000000000..aa0d9a2e6deeb62126074af4ae28d3c00dab17ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +class MXUsersDevicesMap<E> { + + // A map of maps (userId -> (deviceId -> Object)). + val map = HashMap<String /* userId */, HashMap<String /* deviceId */, E>>() + + /** + * @return the user Ids + */ + val userIds: List<String> + get() = map.keys.toList() + + val isEmpty: Boolean + get() = map.isEmpty() + + /** + * Provides the device ids list for a user id + * FIXME Should maybe return emptyList and not null, to avoid many !! in the code + * + * @param userId the user id + * @return the device ids list + */ + fun getUserDeviceIds(userId: String?): List<String>? { + return if (!userId.isNullOrBlank() && map.containsKey(userId)) { + map[userId]!!.keys.toList() + } else null + } + + /** + * Provides the object for a device id and a user Id + * + * @param deviceId the device id + * @param userId the object id + * @return the object + */ + fun getObject(userId: String?, deviceId: String?): E? { + return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) { + map[userId]?.get(deviceId) + } else null + } + + /** + * Set an object for a dedicated user Id and device Id + * + * @param userId the user Id + * @param deviceId the device id + * @param o the object to set + */ + fun setObject(userId: String?, deviceId: String?, o: E?) { + if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) { + val devices = map.getOrPut(userId) { HashMap() } + devices[deviceId] = o + } + } + + /** + * Defines the objects map for a user Id + * + * @param objectsPerDevices the objects maps + * @param userId the user id + */ + fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) { + if (!userId.isNullOrBlank()) { + if (null == objectsPerDevices) { + map.remove(userId) + } else { + map[userId] = HashMap(objectsPerDevices) + } + } + } + + /** + * Removes objects for a dedicated user + * + * @param userId the user id. + */ + fun removeUserObjects(userId: String?) { + if (!userId.isNullOrBlank()) { + map.remove(userId) + } + } + + /** + * Clear the internal dictionary + */ + fun removeAllObjects() { + map.clear() + } + + /** + * Add entries from another MXUsersDevicesMap + * + * @param other the other one + */ + fun addEntriesFromMap(other: MXUsersDevicesMap<E>?) { + if (null != other) { + map.putAll(other.map) + } + } + + override fun toString(): String { + return "MXUsersDevicesMap $map" + } +} + +inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) { + userIds.forEach { userId -> + getUserDeviceIds(userId)?.forEach { deviceId -> + getObject(userId, deviceId)?.let { + action(userId, deviceId, it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt new file mode 100755 index 0000000000000000000000000000000000000000..1621db380d9c3a88fef400ffc22765c57ef8bb2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.io.Serializable + +/** + * This class adds more context to a OlmInboundGroupSession object. + * This allows additional checks. The class implements Serializable so that the context can be stored. + */ +class OlmInboundGroupSessionWrapper : Serializable { + + // The associated olm inbound group session. + var olmInboundGroupSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var roomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var senderKey: String? = null + + // Other keys the sender claims. + var keysClaimed: Map<String, String>? = null + + // Devices which forwarded this session to us (normally empty). + var forwardingCurve25519KeyChain: List<String>? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + } + + return null + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + olmInboundGroupSession = OlmInboundGroupSession(sessionKey) + } else { + olmInboundGroupSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + } + + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { + throw Exception("Mismatched group session Id") + } + + senderKey = megolmSessionData.senderKey + keysClaimed = megolmSessionData.senderClaimedKeys + roomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(): MegolmSessionData? { + return try { + if (null == forwardingCurve25519KeyChain) { + forwardingCurve25519KeyChain = ArrayList() + } + + if (keysClaimed == null) { + return null + } + + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) + } catch (e: Exception) { + Timber.e(e, "## export() : senderKey $senderKey failed") + null + } + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + } + + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt new file mode 100755 index 0000000000000000000000000000000000000000..091106c1617779a9cbdabb39fe339e399bd86fc5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.io.Serializable + +/** + * This class adds more context to a OlmInboundGroupSession object. + * This allows additional checks. The class implements Serializable so that the context can be stored. + */ +class OlmInboundGroupSessionWrapper2 : Serializable { + + // The associated olm inbound group session. + var olmInboundGroupSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var roomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var senderKey: String? = null + + // Other keys the sender claims. + var keysClaimed: Map<String, String>? = null + + // Devices which forwarded this session to us (normally empty). + var forwardingCurve25519KeyChain: List<String>? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + } + + return null + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + olmInboundGroupSession = OlmInboundGroupSession(sessionKey) + } else { + olmInboundGroupSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + } + + constructor() { + // empty + } + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { + throw Exception("Mismatched group session Id") + } + + senderKey = megolmSessionData.senderKey + keysClaimed = megolmSessionData.senderClaimedKeys + roomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * @param index the index to export. If null, the first known index will be used + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + if (null == forwardingCurve25519KeyChain) { + forwardingCurve25519KeyChain = ArrayList() + } + + if (keysClaimed == null) { + return null + } + + val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(wantedIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) + } catch (e: Exception) { + Timber.e(e, "## export() : senderKey $senderKey failed") + null + } + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + } + + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..448043024d9eb5d3d815e1c6883253016a5b1986 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.olm.OlmSession + +/** + * Encapsulate a OlmSession and a last received message Timestamp + */ +data class OlmSessionWrapper( + // The associated olm session. + val olmSession: OlmSession, + // Timestamp at which the session last received a message. + var lastReceivedMessageTs: Long = 0) { + + /** + * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` + */ + fun onMessageReceived() { + lastReceivedMessageTs = System.currentTimeMillis() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..79f86bd28c2ade13a70d0c2891f5843f3e7d0872 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptedEventContent( + + /** + * the used algorithm + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * The encrypted event + */ + @Json(name = "ciphertext") + val ciphertext: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + val deviceId: String? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * The session id + */ + @Json(name = "session_id") + val sessionId: String? = null, + + // Relation context is in clear in encrypted message + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..255e5e8d81ecc9d89e932a673922204ad36e56d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptionEventContent( + /** + * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * How long the session should be used before changing it. 604800000 (a week) is the recommended default. + */ + @Json(name = "rotation_period_ms") + val rotationPeriodMs: Long? = null, + + /** + * How many messages should be sent before changing the session. 100 is the recommended default. + */ + @Json(name = "rotation_period_msgs") + val rotationPeriodMsgs: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..27ccc2d041593389bbb7836f7f4c24dde29cdef7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NewDeviceContent( + // the device id + @Json(name = "device_id") + val deviceId: String? = null, + + // the room ids list + @Json(name = "rooms") + val rooms: List<String>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9de805962941f25b5d8b197e2b62ab8108611fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class OlmEventContent( + /** + * + */ + @Json(name = "ciphertext") + val ciphertext: Map<String, Any>? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + val senderKey: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3a9ee2e5161255c544b88265b156f6c1cd5ad6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class representing the OLM payload content + */ +@JsonClass(generateAdapter = true) +data class OlmPayloadContent( + /** + * The room id + */ + var room_id: String? = null, + + /** + * The sender + */ + var sender: String? = null, + + /** + * The recipient + */ + var recipient: String? = null, + + /** + * the recipient keys + */ + var recipient_keys: Map<String, String>? = null, + + /** + * The keys + */ + var keys: Map<String, String>? = null +) { + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this) + } + + companion object { + fun fromJsonString(str: String): OlmPayloadContent? { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).fromJson(str) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..eeaf52f0e1ed3eec3df4aec3b2d0aad320678db0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyContent( + + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "room_id") + val roomId: String? = null, + + @Json(name = "session_id") + val sessionId: String? = null, + + @Json(name = "session_key") + val sessionKey: String? = null, + + // should be a Long but it is sometimes a double + @Json(name = "chain_index") + val chainIndex: Any? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d9a1937aff0f03f379468b63e76398f82e84496 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyWithHeldContent( + + /** + * Required if code is not m.no_olm. The ID of the room that the session belongs to. + */ + @Json(name = "room_id") val roomId: String? = null, + + /** + * Required. The encryption algorithm that the key is for. + */ + @Json(name = "algorithm") val algorithm: String? = null, + + /** + * Required if code is not m.no_olm. The ID of the session. + */ + @Json(name = "session_id") val sessionId: String? = null, + + /** + * Required. The key of the session creator. + */ + @Json(name = "sender_key") val senderKey: String? = null, + + /** + * Required. A machine-readable code for why the key was not sent + */ + @Json(name = "code") val codeString: String? = null, + + /** + * A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code. + */ + @Json(name = "reason") val reason: String? = null + +) { + val code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } +} + +enum class WithHeldCode(val value: String) { + /** + * the user/device was blacklisted + */ + BLACKLISTED("m.blacklisted"), + /** + * the user/devices is unverified + */ + UNVERIFIED("m.unverified"), + /** + * the user/device is not allowed have the key. For example, this would usually be sent in response + * to a key request if the user was not in the room when the message was sent + */ + UNAUTHORISED("m.unauthorised"), + /** + * Sent in reply to a key request if the device that the key is requested from does not have the requested key + */ + UNAVAILABLE("m.unavailable"), + /** + * An olm session could not be established. + * This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + */ + NO_OLM("m.no_olm"); + + companion object { + fun fromCode(code: String?): WithHeldCode? { + return when (code) { + BLACKLISTED.value -> BLACKLISTED + UNVERIFIED.value -> UNVERIFIED + UNAUTHORISED.value -> UNAUTHORISED + UNAVAILABLE.value -> UNAVAILABLE + NO_OLM.value -> NO_OLM + else -> null + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..5b7a139488c07d30d916307063f7d625259e8134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class SecretSendEventContent( + @Json(name = "request_id") val requestId: String, + @Json(name = "secret") val secretValue: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..4b1530c9c6e5a8a2544b6012358020c70e0829d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete a device + */ +@JsonClass(generateAdapter = true) +internal data class DeleteDeviceParams( + @Json(name = "auth") + val userPasswordAuth: UserPasswordAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..97c7c59b503ccf1ade3df88789192ef88acb7aea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.interfaces.DatedObject + +/** + * This class describes the device information + */ +@JsonClass(generateAdapter = true) +data class DeviceInfo( + /** + * The owner user id (not documented and useless but the homeserver sent it. You should not need it) + */ + @Json(name = "user_id") + val user_id: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + val deviceId: String? = null, + + /** + * The device display name + */ + @Json(name = "display_name") + val displayName: String? = null, + + /** + * The last time this device has been seen. + */ + @Json(name = "last_seen_ts") + val lastSeenTs: Long? = null, + + /** + * The last ip address + */ + @Json(name = "last_seen_ip") + val lastSeenIp: String? = null +) : DatedObject { + + override val date: Long + get() = lastSeenTs ?: 0 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt new file mode 100644 index 0000000000000000000000000000000000000000..efc036c4d8a5e321b9d1e1c5fcdca4b4a20b913e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DeviceKeys( + /** + * Required. The ID of the user the device belongs to. Must match the user ID used when logging in. + */ + @Json(name = "user_id") + val userId: String, + + /** + * Required. The ID of the device these keys belong to. Must match the device ID used when logging in. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * Required. The encryption algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List<String>?, + + /** + * Required. Public identity keys. The names of the properties should be in the format <algorithm>:<device_id>. + * The keys themselves should be encoded as specified by the key algorithm. + */ + @Json(name = "keys") + val keys: Map<String, String>?, + + /** + * Required. Signatures for the device key object. A map from user ID, to a map from <algorithm>:<device_id> to the signature. + * The signature is calculated using the process described at https://matrix.org/docs/spec/appendices.html#signing-json. + */ + @Json(name = "signatures") + val signatures: Map<String, Map<String, String>>? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0f900f6c07ed656710aafc0c3c50dba49f79d0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DeviceKeysWithUnsigned( + /** + * Required. The ID of the user the device belongs to. Must match the user ID used when logging in. + */ + @Json(name = "user_id") + val userId: String, + + /** + * Required. The ID of the device these keys belong to. Must match the device ID used when logging in. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * Required. The encryption algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List<String>?, + + /** + * Required. Public identity keys. The names of the properties should be in the format <algorithm>:<device_id>. + * The keys themselves should be encoded as specified by the key algorithm. + */ + @Json(name = "keys") + val keys: Map<String, String>?, + + /** + * Required. Signatures for the device key object. A map from user ID, to a map from <algorithm>:<device_id> to the signature. + * The signature is calculated using the process described at https://matrix.org/docs/spec/appendices.html#signing-json. + */ + @Json(name = "signatures") + val signatures: Map<String, Map<String, String>>?, + + /** + * Additional data added to the device key information by intermediate servers, and not covered by the signatures. + */ + @Json(name = "unsigned") + val unsigned: UnsignedDeviceInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..934a0cf43c2cc026fa8ca1064df913d79811d6c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the response to https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + */ +@JsonClass(generateAdapter = true) +data class DevicesListResponse( + @Json(name = "devices") + val devices: List<DeviceInfo>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8464566d7cff37619333a15f3caf7d74c17604bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +/** + * Class representing the dummy content + * Ref: https://matrix.org/docs/spec/client_server/latest#id82 + */ +typealias DummyContent = Unit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..56156cf749e7617de1c530818c05abb20a9b663b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.olm.OlmPkMessage + +/** + * Build from a OlmPkMessage object + * + * @param olmPkMessage OlmPkMessage + */ +class EncryptedBodyFileInfo(olmPkMessage: OlmPkMessage) { + var ciphertext = olmPkMessage.mCipherText + var mac = olmPkMessage.mMac + var ephemeral = olmPkMessage.mEphemeralKey +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..65455e9fa3c64ae3058e041a7c3bd1f3d4d75a1d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * In Matrix specs: EncryptedFile + */ +@JsonClass(generateAdapter = true) +data class EncryptedFileInfo( + /** + * Required. The URL to the file. + */ + @Json(name = "url") + val url: String? = null, + + /** + * Not documented + */ + @Json(name = "mimetype") + val mimetype: String? = null, + + /** + * Required. A JSON Web Key object. + */ + @Json(name = "key") + val key: EncryptedFileKey? = null, + + /** + * Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64. + */ + @Json(name = "iv") + val iv: String? = null, + + /** + * Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. + * Clients should support the SHA-256 hash, which uses the key "sha256". + */ + @Json(name = "hashes") + val hashes: Map<String, String>? = null, + + /** + * Required. Version of the encrypted attachments protocol. Must be "v2". + */ + @Json(name = "v") + val v: String? = null +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (url.isNullOrBlank()) { + return false + } + + if (key?.isValid() != true) { + return false + } + + if (iv.isNullOrBlank()) { + return false + } + + if (hashes?.containsKey("sha256") != true) { + return false + } + + if (v != "v2") { + return false + } + + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0a680cfd3b1a05f347301a5690ad480a34487b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedFileKey( + /** + * Required. Algorithm. Must be "A256CTR". + */ + @Json(name = "alg") + val alg: String? = null, + + /** + * Required. Extractable. Must be true. This is a W3C extension. + */ + @Json(name = "ext") + val ext: Boolean? = null, + + /** + * Required. Key operations. Must at least contain "encrypt" and "decrypt". + */ + @Json(name = "key_ops") + val key_ops: List<String>? = null, + + /** + * Required. Key type. Must be "oct". + */ + @Json(name = "kty") + val kty: String? = null, + + /** + * Required. The key, encoded as urlsafe unpadded base64. + */ + @Json(name = "k") + val k: String? = null +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (alg != "A256CTR") { + return false + } + + if (ext != true) { + return false + } + + if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) { + return false + } + + if (kty != "oct") { + return false + } + + if (k.isNullOrBlank()) { + return false + } + + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt new file mode 100644 index 0000000000000000000000000000000000000000..d04481554278cd051c69d0af54abebd7bd434fb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedMessage( + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "sender_key") + val senderKey: String? = null, + + @Json(name = "ciphertext") + val cipherText: Map<String, Any>? = null +) : SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..927828c4a01890141b77f381a71bd05d6d063dcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the forward room key request body content + * Ref: https://matrix.org/docs/spec/client_server/latest#m-forwarded-room-key + */ +@JsonClass(generateAdapter = true) +data class ForwardedRoomKeyContent( + /** + * Required. The encryption algorithm the key in this event is to be used with. + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * Required. The room where the key is used. + */ + @Json(name = "room_id") + val roomId: String? = null, + + /** + * Required. The Curve25519 key of the device which initiated the session originally. + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * Required. The ID of the session that the key is for. + */ + @Json(name = "session_id") + val sessionId: String? = null, + + /** + * Required. The key to be exchanged. + */ + @Json(name = "session_key") + val sessionKey: String? = null, + + /** + * Required. Chain of Curve25519 keys. It starts out empty, but each time the key is forwarded to another device, + * the previous sender in the chain is added to the end of the list. For example, if the key is forwarded + * from A to B to C, this field is empty between A and B, and contains A's Curve25519 key between B and C. + */ + @Json(name = "forwarding_curve25519_key_chain") + val forwardingCurve25519KeyChain: List<String>? = null, + + /** + * Required. The Ed25519 key of the device which initiated the session originally. It is 'claimed' because the + * receiving device has no way to tell that the original room_key actually came from a device which owns the + * private part of this key unless they have done device verification. + */ + @Json(name = "sender_claimed_ed25519_key") + val senderClaimedEd25519Key: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3b156084de9ac83b6f6704cc924d4b53352287f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Interface representing an room key action request + * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] + */ +interface GossipingToDeviceObject : SendToDeviceObject { + + val action: String? + + val requestingDeviceId: String? + + val requestId: String? + + companion object { + const val ACTION_SHARE_REQUEST = "request" + const val ACTION_SHARE_CANCELLATION = "request_cancellation" + } +} + +@JsonClass(generateAdapter = true) +data class GossipingDefaultContent( + @Json(name = "action") override val action: String?, + @Json(name = "requesting_device_id") override val requestingDeviceId: String?, + @Json(name = "m.request_id") override val requestId: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c677c7123d70979249008c47c7652b3232fe8a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the key changes response + */ +@JsonClass(generateAdapter = true) +internal data class KeyChangesResponse( + // list of user ids which have new devices + @Json(name = "changed") + val changed: List<String>? = null, + + // List of user ids who are no more tracked. + @Json(name = "left") + val left: List<String>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4e3dd9297bd4ffd418417b9e3b4a41d5ff62eed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory + +/** + * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationAccept( + /** + * string to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + @Json(name = "transaction_id") + override val transactionId: String? = null, + + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "key_agreement_protocol") + override val keyAgreementProtocol: String? = null, + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "hash") + override val hash: String? = null, + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "message_authentication_code") + override val messageAuthenticationCode: String? = null, + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + @Json(name = "short_authentication_string") + override val shortAuthenticationStrings: List<String>? = null, + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + @Json(name = "commitment") + override var commitment: String? = null +) : SendToDeviceObject, VerificationInfoAccept { + + override fun toSendToDeviceObject() = this + + companion object : VerificationInfoAcceptFactory { + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>): VerificationInfoAccept { + return KeyVerificationAccept( + transactionId = tid, + keyAgreementProtocol = keyAgreementProtocol, + hash = hash, + commitment = commitment, + messageAuthenticationCode = messageAuthenticationCode, + shortAuthenticationStrings = shortAuthenticationStrings + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea2cbf214b1a83daadec7e7a06bd1e47a863e8c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel + +/** + * To device event sent by either party to cancel a key verification. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationCancel( + /** + * the transaction ID of the verification to cancel + */ + @Json(name = "transaction_id") + override val transactionId: String? = null, + + /** + * machine-readable reason for cancelling, see #CancelCode + */ + override val code: String? = null, + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + override val reason: String? = null +) : SendToDeviceObject, VerificationInfoCancel { + + companion object { + fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { + return KeyVerificationCancel( + tid, + cancelCode.value, + cancelCode.humanReadable + ) + } + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4d75a0de6d4a67428e6c4091e87e54e94065bf1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationDone( + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoDone { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf1ded002e8340f491a23501b9a0e697e27ea2ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationKey( + /** + * the ID of the transaction that the message is part of + */ + @Json(name = "transaction_id") override val transactionId: String? = null, + + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null + +) : SendToDeviceObject, VerificationInfoKey { + + companion object : VerificationInfoKeyFactory { + override fun create(tid: String, pubKey: String): KeyVerificationKey { + return KeyVerificationKey(tid, pubKey) + } + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt new file mode 100644 index 0000000000000000000000000000000000000000..001cabaa4e825e979993d0813e60f3ce00f68778 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory + +/** + * Sent by both devices to send the MAC of their device key to the other device. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationMac( + @Json(name = "transaction_id") override val transactionId: String? = null, + @Json(name = "mac") override val mac: Map<String, String>? = null, + @Json(name = "keys") override val keys: String? = null + +) : SendToDeviceObject, VerificationInfoMac { + + override fun toSendToDeviceObject(): SendToDeviceObject? = this + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac { + return KeyVerificationMac(tid, mac, keys) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt new file mode 100644 index 0000000000000000000000000000000000000000..25d6984560093f0d47d8d8d40aada61dff4a5be2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationReady( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List<String>?, + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoReady { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bf1d1a5c2e3f5b31dea7f44d2435dacc7ddeb77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationRequest( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List<String>, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoRequest { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc99c71f09b3ef93a39529b61e467a77ab329e03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +/** + * Sent by Alice to initiate an interactive key verification. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationStart( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "method") override val method: String? = null, + @Json(name = "transaction_id") override val transactionId: String? = null, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null, + @Json(name = "hashes") override val hashes: List<String>? = null, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null, + // For QR code verification + @Json(name = "secret") override val sharedSecret: String? = null +) : SendToDeviceObject, VerificationInfoStart { + + override fun toCanonicalJson(): String { + return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..f48936a80ebc39751ed4a3cf2e292a8602e179e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +internal data class KeysClaimBody( + /** + * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. + */ + @Json(name = "timeout") + val timeout: Int? = null, + + /** + * Required. The keys to be claimed. A map from user ID, to a map from device ID to algorithm name. + */ + @Json(name = "one_time_keys") + val oneTimeKeys: Map<String, Map<String, String>> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1eebcb6946c21806482a68b1fa017f5b8488b0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +internal data class KeysClaimResponse( + /** + * The requested keys ordered by device by user. + * TODO Type does not match spec, should be Map<String, JsonDict> + */ + @Json(name = "one_time_keys") + val oneTimeKeys: Map<String, Map<String, Map<String, Map<String, Any>>>>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..4232225cbefe94e750cdeb98cbc5e4384823451a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the body to /keys/query + */ +@JsonClass(generateAdapter = true) +internal data class KeysQueryBody( + /** + * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. + */ + @Json(name = "timeout") + val timeout: Int? = null, + + /** + * Required. The keys to be downloaded. + * A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user. + */ + @Json(name = "device_keys") + val deviceKeys: Map<String, List<String>>, + + /** + * If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token + * of that sync request, or any later sync token. This allows the server to ensure its response contains the keys advertised + * by the notification in that sync. + */ + @Json(name = "token") + val token: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..a099419c3c3a61366bb71401305a13b1b120ef8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/query request made by downloadKeysForUsers + * + * After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys, + * self_signing_keys and user_signing_keys properties. + * + * The user_signing_keys property will only be included when a user requests their own keys. + */ +@JsonClass(generateAdapter = true) +internal data class KeysQueryResponse( + /** + * Information on the queried devices. A map from user ID, to a map from device ID to device information. + * For each device, the information returned will be the same as uploaded via /keys/upload, with the addition of an unsigned property. + */ + @Json(name = "device_keys") + val deviceKeys: Map<String, Map<String, DeviceKeysWithUnsigned>>? = null, + + /** + * If any remote homeservers could not be reached, they are recorded here. The names of the + * properties are the names of the unreachable servers. + * + * If the homeserver could be reached, but the user or device was unknown, no failure is recorded. + * Instead, the corresponding user or device is missing from the device_keys result. + */ + val failures: Map<String, Map<String, Any>>? = null, + + @Json(name = "master_keys") + val masterKeys: Map<String, RestKeyInfo?>? = null, + + @Json(name = "self_signing_keys") + val selfSigningKeys: Map<String, RestKeyInfo?>? = null, + + @Json(name = "user_signing_keys") + val userSigningKeys: Map<String, RestKeyInfo?>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..3d9652e328362a6a7638c3d81c502d00ec398195 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-upload + */ +@JsonClass(generateAdapter = true) +internal data class KeysUploadBody( + /** + * Identity keys for the device. + * + * May be absent if no new identity keys are required. + */ + @Json(name = "device_keys") + val deviceKeys: DeviceKeys? = null, + + /** + * One-time public keys for "pre-key" messages. The names of the properties should be in the + * format <algorithm>:<key_id>. The format of the key is determined by the key algorithm. + * + * May be absent if no new one-time keys are required. + */ + @Json(name = "one_time_keys") + val oneTimeKeys: JsonDict? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..07904969f039ca378964ee99ea7cce259c76cda6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/upload request made by uploadKeys. + */ +@JsonClass(generateAdapter = true) +internal data class KeysUploadResponse( + /** + * Required. For each key algorithm, the number of unclaimed one-time keys + * of that type currently held on the server for this device. + */ + @Json(name = "one_time_key_counts") + val oneTimeKeyCounts: Map<String, Int>? = null +) { + /** + * Helper methods to extract information from 'oneTimeKeyCounts' + * + * @param algorithm the expected algorithm + * @return the time key counts + */ + fun oneTimeKeyCountsForAlgorithm(algorithm: String): Int { + return oneTimeKeyCounts?.get(algorithm) ?: 0 + } + + /** + * Tells if there is a oneTimeKeys for a dedicated algorithm. + * + * @param algorithm the algorithm + * @return true if it is found + */ + fun hasOneTimeKeyCountsForAlgorithm(algorithm: String): Boolean { + return oneTimeKeyCounts?.containsKey(algorithm) == true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..cfdb300f16c9d9aaebf16983fb171fd462c02bc1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper + +@JsonClass(generateAdapter = true) +internal data class RestKeyInfo( + /** + * The user who owns the key + */ + @Json(name = "user_id") + val userId: String, + + /** + * Allowed uses for the key. + * Must contain "master" for master keys, "self_signing" for self-signing keys, and "user_signing" for user-signing keys. + * See CrossSigningKeyInfo#KEY_USAGE_* constants + */ + @Json(name = "usage") + val usages: List<String>?, + + /** + * An object that must have one entry, + * whose name is "ed25519:" followed by the unpadded base64 encoding of the public key, + * and whose value is the unpadded base64 encoding of the public key. + */ + @Json(name = "keys") + val keys: Map<String, String>?, + + /** + * Signatures of the key. + * A self-signing or user-signing key must be signed by the master key. + * A master key may be signed by a device. + */ + @Json(name = "signatures") + val signatures: Map<String, Map<String, String>>? = null +) { + fun toCryptoModel(): CryptoCrossSigningKey { + return CryptoInfoMapper.map(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..e3a65df5d06dae0df330e5b9e49ab64fd15c91ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class representing an room key request body content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyRequestBody( + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "room_id") + val roomId: String? = null, + + @Json(name = "sender_key") + val senderKey: String? = null, + + @Json(name = "session_id") + val sessionId: String? = null +) { + fun toJson(): String { + return MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).toJson(this) + } + + companion object { + fun fromJson(json: String?): RoomKeyRequestBody? { + return json?.let { MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).fromJson(it) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..299c08481974392d95b8a9fac9e9cb1d4ae1396d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "body") + val body: RoomKeyRequestBody? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..98a586d13684d9a22f0505ba0c49a6698f8df824 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class SecretShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "name") + val secretName: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..e7df20ee5e0cb77a251079ce9b0c183cdb71a256 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +internal data class SendToDeviceBody( + /** + * `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization + * + * The messages to send. A map from user ID, to a map from device ID to message body. + * The device ID may also be *, meaning all known devices for the user. + */ + val messages: Map<String, Map<String, Any>>? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt new file mode 100644 index 0000000000000000000000000000000000000000..a018d62ab4e6b85203909b44aaba4f61713ad300 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +interface SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6449fe8ed5ce959c177ed47d9bbe21e86e766b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION + +/** + * Class representing a room key request cancellation content + */ +@JsonClass(generateAdapter = true) +internal data class ShareRequestCancellation( + @Json(name = "action") + override val action: String? = ACTION_SHARE_CANCELLATION, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..e21fd8fbd4a23e14c13bcbe370809e352a46de7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Upload Signature response + */ +@JsonClass(generateAdapter = true) +internal data class SignatureUploadResponse( + /** + * The response contains a failures property, which is a map of user ID to device ID to failure reason, + * if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + */ + val failures: Map<String, Map<String, UploadResponseFailure>>? = null +) + +@JsonClass(generateAdapter = true) +internal data class UploadResponseFailure( + @Json(name = "status") + val status: Int, + + @Json(name = "errcode") + val errCode: String, + + @Json(name = "message") + val message: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..1fc0d417e8df54f98b988dd0a9e1bd24842aba8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UnsignedDeviceInfo( + /** + * The display name which the user set on the device. + */ + @Json(name = "device_display_name") + val deviceDisplayName: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac691e6e6e728027f0379b71257aaede05e7f56e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UpdateDeviceInfoBody( + /** + * The new display name for this device. If not given, the display name is unchanged. + */ + @Json(name = "display_name") + val displayName: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..dbb2822cdddd510886ba8893ab456c678ab9d20f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.toRest + +/** + * Helper class to build CryptoApi#uploadSignatures params + */ +internal data class UploadSignatureQueryBuilder( + private val deviceInfoList: MutableList<CryptoDeviceInfo> = mutableListOf(), + private val signingKeyInfoList: MutableList<CryptoCrossSigningKey> = mutableListOf() +) { + + fun withDeviceInfo(deviceInfo: CryptoDeviceInfo) = apply { + deviceInfoList.add(deviceInfo) + } + + fun withSigningKeyInfo(info: CryptoCrossSigningKey) = apply { + signingKeyInfoList.add(info) + } + + fun build(): Map<String, Map<String, @JvmSuppressWildcards Any>> { + val map = HashMap<String, HashMap<String, Any>>() + + val usersList = (deviceInfoList.map { it.userId } + signingKeyInfoList.map { it.userId }) + .distinct() + + usersList.forEach { userID -> + val userMap = HashMap<String, Any>() + deviceInfoList.filter { it.userId == userID }.forEach { deviceInfo -> + userMap[deviceInfo.deviceId] = deviceInfo.toRest() + } + signingKeyInfoList.filter { it.userId == userID }.forEach { keyInfo -> + keyInfo.unpaddedBase64PublicKey?.let { base64Key -> + userMap[base64Key] = keyInfo.toRest() + } + } + map[userID] = userMap + } + + return map + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7a61b282fdadf326ee200710ce0363ec541b2e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UploadSigningKeysBody( + @Json(name = "master_key") + val masterKey: RestKeyInfo? = null, + + @Json(name = "self_signing_key") + val selfSigningKey: RestKeyInfo? = null, + + @Json(name = "user_signing_key") + val userSigningKey: RestKeyInfo? = null, + + @Json(name = "auth") + val auth: UserPasswordAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt new file mode 100644 index 0000000000000000000000000000000000000000..018f7071059e0a3aaa1024bd57f5556413259f2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * This class provides the authentication data by using user and password + */ +@JsonClass(generateAdapter = true) +data class UserPasswordAuth( + + // device device session id + @Json(name = "session") + val session: String? = null, + + // registration information + @Json(name = "type") + val type: String? = LoginFlowTypes.PASSWORD, + + @Json(name = "user") + val user: String? = null, + + @Json(name = "password") + val password: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt new file mode 100644 index 0000000000000000000000000000000000000000..99bbebf7eb218fde32b248eb1761a77ae1a4f959 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod + +internal const val VERIFICATION_METHOD_SAS = "m.sas.v1" + +// Qr code +// Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#verification-methods +internal const val VERIFICATION_METHOD_QR_CODE_SHOW = "m.qr_code.show.v1" +internal const val VERIFICATION_METHOD_QR_CODE_SCAN = "m.qr_code.scan.v1" +internal const val VERIFICATION_METHOD_RECIPROCATE = "m.reciprocate.v1" + +internal fun VerificationMethod.toValue(): String { + return when (this) { + VerificationMethod.SAS -> VERIFICATION_METHOD_SAS + VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..20b8ff184044b733721d579e3734cbfabe867976 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.repository + +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class WarnOnUnknownDeviceRepository @Inject constructor() { + + // TODO: set it back to true by default. Need UI + // Warn the user if some new devices are detected while encrypting a message. + private var warnOnUnknownDevices = false + + /** + * Tells if the encryption must fail if some unknown devices are detected. + * + * @return true to warn when some unknown devices are detected. + */ + fun warnOnUnknownDevices() = warnOnUnknownDevices + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + fun setWarnOnUnknownDevices(warn: Boolean) { + warnOnUnknownDevices = warn + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt new file mode 100644 index 0000000000000000000000000000000000000000..7186bc3cd017e9eeed8938c8f78a306b6ef23b3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.secrets + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent +import org.matrix.android.sdk.api.session.securestorage.IntegrityResult +import org.matrix.android.sdk.api.session.securestorage.KeyInfo +import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.KeySigner +import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo +import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec +import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword +import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey +import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 +import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.olm.OlmPkMessage +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import kotlin.experimental.and + +internal class DefaultSharedSecretStorageService @Inject constructor( + @UserId private val userId: String, + private val accountDataService: AccountDataService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : SharedSecretStorageService { + + override fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback<SsssKeyCreationInfo>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val bytes = try { + (key as? RawBytesKeySpec)?.privateKey + ?: ByteArray(32).also { + SecureRandom().nextBytes(it) + } + } catch (failure: Throwable) { + callback.onFailure(failure) + return@launch + } + + val storageKeyContent = SecretStorageKeyContent( + name = keyName, + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = null + ) + + val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback<Unit> { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(bytes), + keySpec = RawBytesKeySpec(bytes) + )) + } + } + ) + } + } + + override fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback<SsssKeyCreationInfo>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) + + val storageKeyContent = SecretStorageKeyContent( + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt) + ) + + val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback<Unit> { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(privatePart.privateKey), + keySpec = RawBytesKeySpec(privatePart.privateKey) + )) + } + } + ) + } + } + + override fun hasKey(keyId: String): Boolean { + return accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") != null + } + + override fun getKey(keyId: String): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(keyId)) + return SecretStorageKeyContent.fromJson(accountData.content)?.let { + KeyInfoResult.Success( + KeyInfo(id = keyId, content = it) + ) + } ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId)) + } + + override fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) { + val existingKey = getKey(keyId) + if (existingKey is KeyInfoResult.Success) { + accountDataService.updateAccountData(DEFAULT_KEY_ID, + mapOf("key" to keyId), + callback + ) + } else { + callback.onFailure(SharedSecretStorageError.UnknownKey(keyId)) + } + } + + override fun getDefaultKey(): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent(DEFAULT_KEY_ID) + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + val keyId = accountData.content["key"] as? String + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + return getKey(keyId) + } + + override fun storeSecret(name: String, secretBase64: String, keys: List<SharedSecretStorageService.KeyRef>, callback: MatrixCallback<Unit>) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val encryptedContents = HashMap<String, EncryptedSecretContent>() + try { + keys.forEach { + val keyId = it.keyId + // encrypt the content + when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) { + is KeyInfoResult.Success -> { + if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) { + encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let { + encryptedContents[key.keyInfo.id] = it + } + } else { + // Unknown algorithm + callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) + return@launch + } + } + is KeyInfoResult.Error -> { + callback.onFailure(key.error) + return@launch + } + } + } + + accountDataService.updateAccountData( + type = name, + content = mapOf( + "encrypted" to encryptedContents + ), + callback = callback + ) + } catch (failure: Throwable) { + callback.onFailure(failure) + } + } + } + + /** + * Encryption algorithm m.secret_storage.v1.aes-hmac-sha2 + * Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows: + * + * Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes + * of 0, and with the secret name as the info. + * + * The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + * + * Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use + * this as the AES initialization vector. + * This becomes the iv property, encoded using base64. + * + * Encrypt the data using AES-CTR-256 using the AES key generated above. + * + * This encrypted data, encoded using base64, becomes the ciphertext property. + * + * Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above. + * The resulting MAC is base64-encoded and becomes the mac property. + * (We use AES-CTR to match file encryption and key exports.) + */ + @Throws + private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val secureRandom = SecureRandom() + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray()) + require(cipherBytes.isNotEmpty()) + + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKeySpec) + val digest = mac.doFinal(cipherBytes) + + return EncryptedSecretContent( + ciphertext = cipherBytes.toBase64NoPadding(), + initializationVector = iv.toBase64NoPadding(), + mac = digest.toBase64NoPadding() + ) + } + + private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16) + + val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText + + // Check Signature + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } + val digest = mac.doFinal(cipherRawBytes) + + if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) { + throw SharedSecretStorageError.BadMac + } + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val decryptedSecret = cipher.doFinal(cipherRawBytes) + + require(decryptedSecret.isNotEmpty()) + + return String(decryptedSecret, Charsets.UTF_8) + } + + override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> { + val accountData = accountDataService.getAccountDataEvent(name) + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.UnknownSecret(name))) + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.SecretNotEncrypted(name))) + + val results = ArrayList<KeyInfoResult>() + encryptedContent.keys.forEach { + (it as? String)?.let { keyId -> + results.add(getKey(keyId)) + } + } + return results + } + + override fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>) { + val accountData = accountDataService.getAccountDataEvent(name) ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownSecret(name)) + } + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name)) + } + val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownKey(name)) + } + + val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id)) + } + + val secretContent = EncryptedSecretContent.fromJson(encryptedForKey) + ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.ParsingError) + } + + val algorithm = key.keyInfo.content + if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + // decrypt from recovery key + withOlmDecryption { olmPkDecryption -> + olmPkDecryption.setPrivateKey(keySpec.privateKey) + olmPkDecryption.decrypt(OlmPkMessage() + .apply { + mCipherText = secretContent.ciphertext + mEphemeralKey = secretContent.ephemeral + mMac = secretContent.mac + } + ) + } + }.foldToCallback(callback) + } + } else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + decryptAesHmacSha2(keySpec, name, secretContent) + }.foldToCallback(callback) + } + } else { + callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) + } + } + + companion object { + const val KEY_ID_BASE = "m.secret_storage.key" + const val ENCRYPTED = "encrypted" + const val DEFAULT_KEY_ID = "m.secret_storage.default_key" + } + + override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult { + if (secretNames.isEmpty()) { + return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret("none")) + } + + val keyInfoResult = if (keyId == null) { + getDefaultKey() + } else { + getKey(keyId) + } + + val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) + + if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 + && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { + // Unsupported algorithm + return IntegrityResult.Error( + SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") + ) + } + + secretNames.forEach { secretName -> + val secretEvent = accountDataService.getAccountDataEvent(secretName) + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret(secretName)) + if ((secretEvent.content["encrypted"] as? Map<*, *>)?.get(keyInfo.id) == null) { + return IntegrityResult.Error(SharedSecretStorageError.SecretNotEncryptedWithKey(secretName, keyInfo.id)) + } + } + + return IntegrityResult.Success(keyInfo.content.passphrase != null) + } + + override fun requestSecret(name: String, myOtherDeviceId: String) { + outgoingGossipingRequestManager.sendSecretShareRequest( + name, + mapOf(userId to listOf(myOtherDeviceId)) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..f248e464c2693ce1c31c7f46bf306d220a926b26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -0,0 +1,440 @@ + +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.olm.OlmAccount + +/** + * the crypto data store + */ +internal interface IMXCryptoStore { + + /** + * @return the device id + */ + fun getDeviceId(): String + + /** + * @return the olm account + */ + fun getOlmAccount(): OlmAccount + + fun getOrCreateOlmAccount(): OlmAccount + + /** + * Retrieve the known inbound group sessions. + * + * @return the list of all known group sessions, to export them. + */ + fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> + + /** + * @return true to unilaterally blacklist all unverified devices. + */ + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + /** + * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @return the room Ids list + */ + fun getRoomsListBlacklistUnverifiedDevices(): List<String> + + /** + * Updates the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @param roomIds the room ids list + */ + fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) + + /** + * Get the current keys backup version + */ + fun getKeyBackupVersion(): String? + + /** + * Set the current keys backup version + * + * @param keyBackupVersion the keys backup version or null to delete it + */ + fun setKeyBackupVersion(keyBackupVersion: String?) + + /** + * Get the current keys backup local data + */ + fun getKeysBackupData(): KeysBackupDataEntity? + + /** + * Set the keys backup local data + * + * @param keysBackupData the keys backup local data, or null to erase data + */ + fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) + + /** + * @return the devices statuses map (userId -> tracking status) + */ + fun getDeviceTrackingStatuses(): Map<String, Int> + + /** + * @return the pending IncomingRoomKeyRequest requests + */ + fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> + + fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) +// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> + + /** + * Indicate if the store contains data for the passed account. + * + * @return true means that the user enabled the crypto in a previous session + */ + fun hasData(): Boolean + + /** + * Delete the crypto store for the passed credentials. + */ + fun deleteStore() + + /** + * open any existing crypto store + */ + fun open() + + /** + * Close the store + */ + fun close() + + /** + * Store the device id. + * + * @param deviceId the device id + */ + fun storeDeviceId(deviceId: String) + + /** + * Store the end to end account for the logged-in user. + * + * @param account the account to save + */ + fun saveOlmAccount() + + /** + * Retrieve a device for a user. + * + * @param deviceId the device id. + * @param userId the user's id. + * @return the device + */ + fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? + + /** + * Retrieve a device by its identity key. + * + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? + + /** + * Store the known devices for a user. + * + * @param userId The user's id. + * @param devices A map from device id to 'MXDevice' object for the device. + */ + fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) + + fun storeUserCrossSigningKeys(userId: String, + masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) + + /** + * Retrieve the known devices for a user. + * + * @param userId The user's id. + * @return The devices map if some devices are known, else null + */ + fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? + + fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? + + fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>> + + fun getLiveDeviceList(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> + + // TODO temp + fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> + + fun getMyDevicesInfo() : List<DeviceInfo> + + fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>> + + fun saveMyDevicesInfo(info: List<DeviceInfo>) + /** + * Store the crypto algorithm for a room. + * + * @param roomId the id of the room. + * @param algorithm the algorithm. + */ + fun storeRoomAlgorithm(roomId: String, algorithm: String) + + /** + * Provides the algorithm used in a dedicated room. + * + * @param roomId the room id + * @return the algorithm, null is the room is not encrypted + */ + fun getRoomAlgorithm(roomId: String): String? + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + + /** + * Store a session between the logged-in user and another device. + * + * @param olmSessionWrapper the end-to-end session. + * @param deviceKey the public key of the other device. + */ + fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) + + /** + * Retrieve the end-to-end session ids between the logged-in user and another + * device. + * + * @param deviceKey the public key of the other device. + * @return A set of sessionId, or null if device is not known + */ + fun getDeviceSessionIds(deviceKey: String): Set<String>? + + /** + * Retrieve an end-to-end session between the logged-in user and another + * device. + * + * @param sessionId the session Id. + * @param deviceKey the public key of the other device. + * @return The Base64 end-to-end session, or null if not found + */ + fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? + + /** + * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist + * + * @param deviceKey the public key of the other device. + * @return last used sessionId, or null if not found + */ + fun getLastUsedSessionId(deviceKey: String): String? + + /** + * Store inbound group sessions. + * + * @param sessions the inbound group sessions to store. + */ + fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) + + /** + * Retrieve an inbound group session. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + */ + fun removeInboundGroupSession(sessionId: String, senderKey: String) + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + /** + * Mark all inbound group sessions as not backed up. + */ + fun resetBackupMarkers() + + /** + * Mark inbound group sessions as backed up on the user homeserver. + * + * @param sessions the sessions + */ + fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) + + /** + * Retrieve inbound group sessions that are not yet backed up. + * + * @param limit the maximum number of sessions to return. + * @return an array of non backed up inbound group sessions. + */ + fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2> + + /** + * Number of stored inbound group sessions. + * + * @param onlyBackedUp if true, count only session marked as backed up. + * @return a count. + */ + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + /** + * Save the device statuses + * + * @param deviceTrackingStatuses the device tracking statuses + */ + fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) + + /** + * Get the tracking status of a specified userId devices. + * + * @param userId the user id + * @param defaultValue the default value + * @return the tracking status + */ + fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int + + /** + * Look for an existing outgoing room key request, and if none is found, return null + * + * @param requestBody the request body + * @return an OutgoingRoomKeyRequest instance or null + */ + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + + /** + * Look for an existing outgoing room key request, and if none is found, add a new one. + * + * @param request the request + * @return either the same instance as passed in, or the existing one. + */ + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? + + fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? + + fun saveGossipingEvent(event: Event) + + fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) + + /** + * Search an IncomingRoomKeyRequest + * + * @param userId the user id + * @param deviceId the device id + * @param requestId the request id + * @return an IncomingRoomKeyRequest if it exists, else null + */ + fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? + + fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + + fun addNewSessionListener(listener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) + + // ============================================= + // CROSS SIGNING + // ============================================= + + /** + * Gets the current crosssigning info + */ + fun getMyCrossSigningInfo(): MXCrossSigningInfo? + + fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) + + fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? + fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>> + fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) + + fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) + + fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) + fun storeMSKPrivateKey(msk: String?) + fun storeSSKPrivateKey(ssk: String?) + fun storeUSKPrivateKey(usk: String?) + + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> + + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) + + fun clearOtherUserTrust() + + fun updateUsersTrust(check: (String) -> Boolean) + + fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? + + fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String) : SharedSessionResult + data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int> + // Dev tools + + fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> + fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> + fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? + fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> + fun getGossipingEventsTrail(): List<Event> + + fun setDeviceKeysUploaded(uploaded: Boolean) + fun getDeviceKeysUploaded(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c8476ea1fa520152f1f1fe2413691608ffddfb6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +data class PrivateKeysInfo( + val master: String? = null, + val selfSigned: String? = null, + val user: String? = null +) { + fun allKnown() = master != null && selfSigned != null && user != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d0c53a5844e1929c16bf7d4af1ed0898450774c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +data class SavedKeyBackupKeyInfo( + val recoveryKey : String, + val version: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt new file mode 100644 index 0000000000000000000000000000000000000000..98098686cc8aa4507c735eee86b8896a8040719e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import android.util.Base64 +import org.matrix.android.sdk.internal.util.CompatUtil +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmObject +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectOutputStream +import java.util.zip.GZIPInputStream + +/** + * Get realm, invoke the action, close realm, and return the result of the action + */ +fun <T> doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T): T { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm) + } +} + +/** + * Get realm, do the query, copy from realm, close realm, and return the copied result + */ +fun <T : RealmObject> doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm)?.let { realm.copyFromRealm(it) } + } +} + +/** + * Get realm, do the list query, copy from realm, close realm, and return the copied result + */ +fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable<T>): Iterable<T> { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm).let { realm.copyFromRealm(it) } + } +} + +/** + * Get realm instance, invoke the action in a transaction and close realm + */ +fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { action.invoke(it) } + } +} + +fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransactionAsync { action.invoke(it) } + } +} + +/** + * Serialize any Serializable object, zip it and convert to Base64 String + */ +fun serializeForRealm(o: Any?): String? { + if (o == null) { + return null + } + + val baos = ByteArrayOutputStream() + val gzis = CompatUtil.createGzipOutputStream(baos) + val out = ObjectOutputStream(gzis) + out.use { + it.writeObject(o) + } + return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) +} + +/** + * Do the opposite of serializeForRealm. + */ +@Suppress("UNCHECKED_CAST") +fun <T> deserializeFromRealm(string: String?): T? { + if (string == null) { + return null + } + val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) + + val bais = ByteArrayInputStream(decodedB64) + val gzis = GZIPInputStream(bais) + val ois = SafeObjectInputStream(gzis) + return ois.use { + it.readObject() as T + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7b9503a842295081bbcf18eabd97dd94633193c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -0,0 +1,1513 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.toEntity +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.query.create +import org.matrix.android.sdk.internal.crypto.store.db.query.delete +import org.matrix.android.sdk.internal.crypto.store.db.query.get +import org.matrix.android.sdk.internal.crypto.store.db.query.getById +import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import timber.log.Timber +import javax.inject.Inject +import kotlin.collections.set + +@SessionScope +internal class RealmCryptoStore @Inject constructor( + @CryptoDatabase private val realmConfiguration: RealmConfiguration, + private val crossSigningKeysMapper: CrossSigningKeysMapper, + private val credentials: Credentials) : IMXCryptoStore { + + /* ========================================================================================== + * Memory cache, to correctly release JNI objects + * ========================================================================================== */ + + // A realm instance, for faster future getInstance. Do not use it + private var realmLocker: Realm? = null + + // The olm account + private var olmAccount: OlmAccount? = null + + // Cache for OlmSession, to release them properly + private val olmSessionsToRelease = HashMap<String, OlmSessionWrapper>() + + // Cache for InboundGroupSession, to release them properly + private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper2>() + + private val newSessionListeners = ArrayList<NewSessionListener>() + + override fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + + private val monarchy = Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .build() + + init { + // Ensure CryptoMetadataEntity is inserted in DB + doRealmTransaction(realmConfiguration) { realm -> + var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst() + + var deleteAll = false + + if (currentMetadata != null) { + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + if (currentMetadata.userId != credentials.userId + || (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) { + Timber.w("## open() : Credentials do not match, close this store and delete data") + deleteAll = true + currentMetadata = null + } + } + + if (currentMetadata == null) { + if (deleteAll) { + realm.deleteAll() + } + + // Metadata not found, or database cleaned, create it + realm.createObject(CryptoMetadataEntity::class.java, credentials.userId).apply { + deviceId = credentials.deviceId + } + } + } + } + /* ========================================================================================== + * Other data + * ========================================================================================== */ + + override fun hasData(): Boolean { + return doWithRealm(realmConfiguration) { + !it.isEmpty + // Check if there is a MetaData object + && it.where<CryptoMetadataEntity>().count() > 0 + } + } + + override fun deleteStore() { + doRealmTransaction(realmConfiguration) { + it.deleteAll() + } + } + + override fun open() { + synchronized(this) { + if (realmLocker == null) { + realmLocker = Realm.getInstance(realmConfiguration) + } + } + } + + override fun close() { + olmSessionsToRelease.forEach { + it.value.olmSession.releaseSession() + } + olmSessionsToRelease.clear() + + inboundGroupSessionToRelease.forEach { + it.value.olmInboundGroupSession?.releaseSession() + } + inboundGroupSessionToRelease.clear() + + olmAccount?.releaseAccount() + + realmLocker?.close() + realmLocker = null + } + + override fun storeDeviceId(deviceId: String) { + doRealmTransaction(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.deviceId = deviceId + } + } + + override fun getDeviceId(): String { + return doWithRealm(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.deviceId + } ?: "" + } + + override fun saveOlmAccount() { + doRealmTransaction(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount) + } + } + + override fun getOlmAccount(): OlmAccount { + return olmAccount!! + } + + override fun getOrCreateOlmAccount(): OlmAccount { + doRealmTransaction(realmConfiguration) { + val metaData = it.where<CryptoMetadataEntity>().findFirst() + val existing = metaData!!.getOlmAccount() + if (existing == null) { + Timber.d("## Crypto Creating olm account") + val created = OlmAccount() + metaData.putOlmAccount(created) + olmAccount = created + } else { + Timber.d("## Crypto Access existing account") + olmAccount = existing + } + } + return olmAccount!! + } + + override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { + return doWithRealm(realmConfiguration) { + it.where<DeviceInfoEntity>() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst() + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { + return doWithRealm(realmConfiguration) { + it.where<DeviceInfoEntity>() + .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) + .findFirst() + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) { + doRealmTransaction(realmConfiguration) { realm -> + if (devices == null) { + // Remove the user + UserEntity.delete(realm, userId) + } else { + UserEntity.getOrCreate(realm, userId) + .let { u -> + // Add the devices + val currentKnownDevices = u.devices.toList() + val new = devices.map { entry -> entry.value.toEntity() } + new.forEach { entity -> + // Maintain first time seen + val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey } + entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis() + realm.insertOrUpdate(entity) + } + // Ensure all other devices are deleted + u.devices.deleteAllFromRealm() + u.devices.addAll(new) + } + } + } + } + + override fun storeUserCrossSigningKeys(userId: String, + masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) { + doRealmTransaction(realmConfiguration) { realm -> + UserEntity.getOrCreate(realm, userId) + .let { userEntity -> + if (masterKey == null || selfSigningKey == null) { + // The user has disabled cross signing? + userEntity.crossSigningInfoEntity?.deleteFromRealm() + userEntity.crossSigningInfoEntity = null + } else { + CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> + // What should we do if we detect a change of the keys? + val existingMaster = signingInfo.getMasterKey() + if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingMaster, masterKey) + } else { + val keyEntity = crossSigningKeysMapper.map(masterKey) + signingInfo.setMasterKey(keyEntity) + } + + val existingSelfSigned = signingInfo.getSelfSignedKey() + if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey) + } else { + val keyEntity = crossSigningKeysMapper.map(selfSigningKey) + signingInfo.setSelfSignedKey(keyEntity) + } + + // Only for me + if (userSigningKey != null) { + val existingUSK = signingInfo.getUserSigningKey() + if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingUSK, userSigningKey) + } else { + val keyEntity = crossSigningKeysMapper.map(userSigningKey) + signingInfo.setUserSignedKey(keyEntity) + } + } + userEntity.crossSigningInfoEntity = signingInfo + } + } + } + } + } + + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return doWithRealm(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>() + .findFirst() + ?.let { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + } + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where<CryptoMetadataEntity>() + }, + { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.apply { + xSignMasterPrivateKey = msk + xSignUserPrivateKey = usk + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.apply { + keyBackupRecoveryKey = recoveryKey + keyBackupRecoveryKeyVersion = version + } + } + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return doWithRealm(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>() + .findFirst() + ?.let { + val key = it.keyBackupRecoveryKey + val version = it.keyBackupRecoveryKeyVersion + if (!key.isNullOrBlank() && !version.isNullOrBlank()) { + SavedKeyBackupKeyInfo(recoveryKey = key, version = version) + } else { + null + } + } + } + } + + override fun storeMSKPrivateKey(msk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.apply { + xSignMasterPrivateKey = msk + } + } + } + + override fun storeSSKPrivateKey(ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.apply { + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun storeUSKPrivateKey(usk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.apply { + xSignUserPrivateKey = usk + } + } + } + + override fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>? { + return doWithRealm(realmConfiguration) { + it.where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + ?.associateBy { cryptoDevice -> + cryptoDevice.deviceId + } + } + } + + override fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>? { + return doWithRealm(realmConfiguration) { + it.where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + override fun getLiveDeviceList(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where<UserEntity>() + .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray()) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.flatten() + } + } + + override fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where<UserEntity>() + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + override fun getMyDevicesInfo(): List<DeviceInfo> { + return monarchy.fetchAllCopiedSync { + it.where<MyDeviceLastSeenInfoEntity>() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where<MyDeviceLastSeenInfoEntity>() + }, + { entity -> + DeviceInfo( + deviceId = entity.deviceId, + lastSeenIp = entity.lastSeenIp, + lastSeenTs = entity.lastSeenTs, + displayName = entity.displayName + ) + } + ) + } + + override fun saveMyDevicesInfo(info: List<DeviceInfo>) { + val entities = info.map { + MyDeviceLastSeenInfoEntity( + lastSeenTs = it.lastSeenTs, + lastSeenIp = it.lastSeenIp, + displayName = it.displayName, + deviceId = it.deviceId + ) + } + monarchy.writeAsync { realm -> + realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm + } + } + + override fun getRoomAlgorithm(roomId: String): String? { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.algorithm + } + } + + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers + } + ?: false + } + + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers + } + } + + override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { + var sessionIdentifier: String? = null + + try { + sessionIdentifier = olmSessionWrapper.olmSession.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeSession() : sessionIdentifier failed") + } + + if (sessionIdentifier != null) { + val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) + + // Release memory of previously known session, if it is not the same one + if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) { + olmSessionsToRelease[key]?.olmSession?.releaseSession() + } + + olmSessionsToRelease[key] = olmSessionWrapper + + doRealmTransaction(realmConfiguration) { + val realmOlmSession = OlmSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + this.deviceKey = deviceKey + putOlmSession(olmSessionWrapper.olmSession) + lastReceivedMessageTs = olmSessionWrapper.lastReceivedMessageTs + } + + it.insertOrUpdate(realmOlmSession) + } + } + } + + override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { + val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) + + // If not in cache (or not found), try to read it from realm + if (olmSessionsToRelease[key] == null) { + doRealmQueryAndCopy(realmConfiguration) { + it.where<OlmSessionEntity>() + .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + } + ?.let { + val olmSession = it.getOlmSession() + if (olmSession != null && it.sessionId != null) { + olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) + } + } + } + + return olmSessionsToRelease[key] + } + + override fun getLastUsedSessionId(deviceKey: String): String? { + return doWithRealm(realmConfiguration) { + it.where<OlmSessionEntity>() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) + .findFirst() + ?.sessionId + } + } + + override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> { + return doWithRealm(realmConfiguration) { + it.where<OlmSessionEntity>() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .findAll() + .mapNotNull { sessionEntity -> + sessionEntity.sessionId + } + } + .toMutableSet() + } + + override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) { + if (sessions.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { realm -> + sessions.forEach { session -> + var sessionIdentifier: String? = null + + try { + sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") + } + + if (sessionIdentifier != null) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) + + // Release memory of previously known session, if it is not the same one + if (inboundGroupSessionToRelease[key] != session) { + inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() + } + + inboundGroupSessionToRelease[key] = session + + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.senderKey + putInboundGroupSession(session) + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } + } + } + } + + override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // If not in cache (or not found), try to read it from realm + if (inboundGroupSessionToRelease[key] == null) { + doWithRealm(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.getInboundGroupSession() + } + ?.let { + inboundGroupSessionToRelease[key] = it + } + } + + return inboundGroupSessionToRelease[key] + } + + /** + * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, + * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management + */ + override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper2> { + return doWithRealm(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .findAll() + .mapNotNull { inboundGroupSessionEntity -> + inboundGroupSessionEntity.getInboundGroupSession() + } + } + .toMutableList() + } + + override fun removeInboundGroupSession(sessionId: String, senderKey: String) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // Release memory of previously known session + inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() + inboundGroupSessionToRelease.remove(key) + + doRealmTransaction(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findAll() + .deleteAllFromRealm() + } + } + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + override fun getKeyBackupVersion(): String? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst() + }?.backupVersion + } + + override fun setKeyBackupVersion(keyBackupVersion: String?) { + doRealmTransaction(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.backupVersion = keyBackupVersion + } + } + + override fun getKeysBackupData(): KeysBackupDataEntity? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where<KeysBackupDataEntity>().findFirst() + } + } + + override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { + doRealmTransaction(realmConfiguration) { + if (keysBackupData == null) { + // Clear the table + it.where<KeysBackupDataEntity>() + .findAll() + .deleteAllFromRealm() + } else { + // Only one object + it.copyToRealmOrUpdate(keysBackupData) + } + } + } + + override fun resetBackupMarkers() { + doRealmTransaction(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .findAll() + .map { inboundGroupSession -> + inboundGroupSession.backedUp = false + } + } + } + + override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) { + if (olmInboundGroupSessionWrappers.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { + olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> + try { + val key = OlmInboundGroupSessionEntity.createPrimaryKey( + olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), + olmInboundGroupSessionWrapper.senderKey) + + it.where<OlmInboundGroupSessionEntity>() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.backedUp = true + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + } + } + + override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2> { + return doWithRealm(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) + .limit(limit.toLong()) + .findAll() + .mapNotNull { inboundGroupSession -> + inboundGroupSession.getInboundGroupSession() + } + } + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return doWithRealm(realmConfiguration) { + it.where<OlmInboundGroupSessionEntity>() + .apply { + if (onlyBackedUp) { + equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true) + } + } + .count() + .toInt() + } + } + + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices = block + } + } + + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return doWithRealm(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices + } ?: false + } + + override fun setDeviceKeysUploaded(uploaded: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded + } + } + + override fun getDeviceKeysUploaded(): Boolean { + return doWithRealm(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer + } ?: false + } + + override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) { + doRealmTransaction(realmConfiguration) { + // Reset all + it.where<CryptoRoomEntity>() + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = false + } + + // Enable those in the list + it.where<CryptoRoomEntity>() + .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray()) + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = true + } + } + } + + override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> { + return doWithRealm(realmConfiguration) { + it.where<CryptoRoomEntity>() + .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) + .findAll() + .mapNotNull { cryptoRoom -> + cryptoRoom.roomId + } + } + .toMutableList() + } + + override fun getDeviceTrackingStatuses(): MutableMap<String, Int> { + return doWithRealm(realmConfiguration) { + it.where<UserEntity>() + .findAll() + .associateBy { user -> + user.userId!! + } + .mapValues { entry -> + entry.value.deviceTrackingStatus + } + } + .toMutableMap() + } + + override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) { + doRealmTransaction(realmConfiguration) { + deviceTrackingStatuses + .map { entry -> + UserEntity.getOrCreate(it, entry.key) + .deviceTrackingStatus = entry.value + } + } + } + + override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { + return doWithRealm(realmConfiguration) { + it.where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.deviceTrackingStatus + } + ?: defaultValue + } + + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + return monarchy.fetchAllCopiedSync { realm -> + realm.where<OutgoingGossipingRequestEntity>() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + && it.requestBody?.roomId == requestBody.roomId + && it.requestBody?.senderKey == requestBody.senderKey + && it.requestBody?.sessionId == requestBody.sessionId + } + } + + override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + return monarchy.fetchAllCopiedSync { realm -> + realm.where<OutgoingGossipingRequestEntity>() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + } + + override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { + return monarchy.fetchAllCopiedSync { realm -> + realm.where<IncomingGossipingRequestEntity>() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + } + } + + override fun getGossipingEventsTrail(): List<Event> { + return monarchy.fetchAllCopiedSync { realm -> + realm.where<GossipingEventEntity>() + }.map { + it.toModel() + } + } + + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? { + // Insert the request and return the one passed in parameter + var request: OutgoingRoomKeyRequest? = null + doRealmTransaction(realmConfiguration) { realm -> + + val existing = realm.where<OutgoingGossipingRequestEntity>() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + && it.requestBody?.sessionId == requestBody.sessionId + && it.requestBody?.senderKey == requestBody.senderKey + && it.requestBody?.roomId == requestBody.roomId + } + + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.requestId = LocalEcho.createLocalEchoId() + this.setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.type = GossipRequestType.KEY + this.requestedInfoStr = requestBody.toJson() + }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + } else { + request = existing + } + } + return request + } + + override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? { + var request: OutgoingSecretRequest? = null + + // Insert the request and return the one passed in parameter + doRealmTransaction(realmConfiguration) { realm -> + val existing = realm.where<OutgoingGossipingRequestEntity>() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.type = GossipRequestType.SECRET + setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.requestId = LocalEcho.createLocalEchoId() + this.requestedInfoStr = secretName + }.toOutgoingGossipingRequest() as? OutgoingSecretRequest + } else { + request = existing + } + } + + return request + } + + override fun saveGossipingEvent(event: Event) { + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + doRealmTransaction(realmConfiguration) { realm -> + realm.insertOrUpdate(entity) + } + } + +// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { realm -> +// realm.where<GossipingEventEntity>() +// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter {entity -> +// states.any { it == entity.requestState} +// } +// }.mapNotNull { +// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>() +// } +// ?.toOutgoingRoomKeyRequest() +// } +// +// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { +// it.where<OutgoingSecretRequestEntity>() +// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) +// .findFirst() +// } +// ?.toOutgoingSecretRequest() +// } + +// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { +// doRealmTransaction(realmConfiguration) { +// val obj = OutgoingRoomKeyRequestEntity().apply { +// requestId = request.requestId +// cancellationTxnId = request.cancellationTxnId +// state = request.state.ordinal +// putRecipients(request.recipients) +// putRequestBody(request.requestBody) +// } +// +// it.insertOrUpdate(obj) +// } +// } + +// override fun deleteOutgoingRoomKeyRequest(transactionId: String) { +// doRealmTransaction(realmConfiguration) { +// it.where<OutgoingRoomKeyRequestEntity>() +// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) +// .findFirst() +// ?.deleteFromRealm() +// } +// } + +// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { +// if (incomingRoomKeyRequest == null) { +// return +// } +// +// doRealmTransaction(realmConfiguration) { +// // Delete any previous store request with the same parameters +// it.where<IncomingRoomKeyRequestEntity>() +// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// .findAll() +// .deleteAllFromRealm() +// +// // Then store it +// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { +// userId = incomingRoomKeyRequest.userId +// deviceId = incomingRoomKeyRequest.deviceId +// requestId = incomingRoomKeyRequest.requestId +// putRequestBody(incomingRoomKeyRequest.requestBody) +// } +// } +// } + +// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { +// doRealmTransaction(realmConfiguration) { +// it.where<GossipingEventEntity>() +// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) +// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter { +// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let { +// +// } +// } +// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// // .findAll() +// // .deleteAllFromRealm() +// } +// } + + override fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<IncomingGossipingRequestEntity>() + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, request.userId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, request.deviceId) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, request.requestId) + .findAll().forEach { + it.requestState = state + } + } + } + + override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<OutgoingGossipingRequestEntity>() + .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) + .findAll().forEach { + it.requestState = state + } + } + } + + override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { + return doWithRealm(realmConfiguration) { realm -> + realm.where<IncomingGossipingRequestEntity>() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) + .findAll() + .mapNotNull { entity -> + entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + } + .firstOrNull() + } + } + + override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { + return doWithRealm(realmConfiguration) { + it.where<IncomingGossipingRequestEntity>() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + .map { entity -> + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + } + } + + override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> { + return doWithRealm(realmConfiguration) { + it.where<IncomingGossipingRequestEntity>() + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + .mapNotNull { entity -> + when (entity.type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + secretName = entity.getRequestedSecretName(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + } + } + } + } + + override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { + doRealmTransactionAsync(realmConfiguration) { realm -> + + // After a clear cache, we might have a + + realm.createObject(IncomingGossipingRequestEntity::class.java).let { + it.otherDeviceId = request.deviceId + it.otherUserId = request.userId + it.requestId = request.requestId ?: "" + it.requestState = GossipingRequestState.PENDING + it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis() + if (request is IncomingSecretShareRequest) { + it.type = GossipRequestType.SECRET + it.requestedInfoStr = request.secretName + } else if (request is IncomingRoomKeyRequest) { + it.type = GossipRequestType.KEY + it.requestedInfoStr = request.requestBody?.toJson() + } + } + } + } + +// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> { +// return doRealmQueryAndCopyList(realmConfiguration) { +// it.where<GossipingEventEntity>() +// .findAll() +// }.map { +// it.toIncomingSecretShareRequest() +// } +// } + + /* ========================================================================================== + * Cross Signing + * ========================================================================================== */ + override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { + return doWithRealm(realmConfiguration) { + it.where<CryptoMetadataEntity>().findFirst()?.userId + }?.let { + getCrossSigningInfo(it) + } + } + + override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { userId -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + } + + override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + xInfoEntity?.crossSigningKeys?.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + newLevel.crossSignedVerified = trusted + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + level.crossSignedVerified = trusted + } + } + } + } + + override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where(DeviceInfoEntity::class.java) + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst()?.let { deviceInfoEntity -> + val trustEntity = deviceInfoEntity.trustLevelEntity + if (trustEntity == null) { + realm.createObject(TrustLevelEntity::class.java).let { + it.locallyVerified = locallyVerified + it.crossSignedVerified = crossSignedVerified + deviceInfoEntity.trustLevelEntity = it + } + } else { + locallyVerified?.let { trustEntity.locallyVerified = it } + trustEntity.crossSignedVerified = crossSignedVerified + } + } + } + } + + override fun clearOtherUserTrust() { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { info -> + // Need to ignore mine + if (info.userId != credentials.userId) { + info.crossSigningKeys.forEach { + it.trustLevelEntity = null + } + } + } + } + } + + override fun updateUsersTrust(check: (String) -> Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { xInfoEntity -> + // Need to ignore mine + if (xInfoEntity.userId == credentials.userId) return@forEach + val mapped = mapCrossSigningInfoEntity(xInfoEntity) + val currentTrust = mapped.isTrusted() + val newTrust = check(mapped.userId) + if (currentTrust != newTrust) { + xInfoEntity.crossSigningKeys.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = newTrust + newLevel.crossSignedVerified = newTrust + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = newTrust + level.crossSignedVerified = newTrust + } + } + } + } + } + } + + override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }) + .filterNotNull() + } + + override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }) + .filterNotNull() + } + + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { + return doWithRealm(realmConfiguration) { realm -> + val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + if (crossSigningInfo == null) { + null + } else { + mapCrossSigningInfoEntity(crossSigningInfo) + } + } + } + + private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + val userId = xsignInfo.userId ?: "" + return MXCrossSigningInfo( + userId = userId, + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + crossSigningKeysMapper.map(userId, it) + } + ) + } + + override fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where<CrossSigningInfoEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { mapCrossSigningInfoEntity(it) } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + + override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { myUserId -> + CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> + val level = xInfoEntity.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + xInfoEntity.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + } + } + } + } + } + + private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { + if (info == null) { + // Delete known if needed + CrossSigningInfoEntity.get(realm, userId)?.deleteFromRealm() + return null + // TODO notify, we might need to untrust things? + } else { + // Just override existing, caller should check and untrust id needed + val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) + existing.crossSigningKeys.deleteAllFromRealm() + existing.crossSigningKeys.addAll( + info.crossSigningKeys.map { + crossSigningKeysMapper.map(it) + } + ) + return existing + } + } + + override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) { + val roomId = withHeldContent.roomId ?: return + val sessionId = withHeldContent.sessionId ?: return + if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return + doRealmTransaction(realmConfiguration) { realm -> + WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { + it.code = withHeldContent.code + it.senderKey = withHeldContent.senderKey + it.reason = withHeldContent.reason + } + } + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return doWithRealm(realmConfiguration) { realm -> + WithHeldSessionEntity.get(realm, roomId, sessionId)?.let { + RoomKeyWithHeldContent( + roomId = roomId, + sessionId = sessionId, + algorithm = it.algorithm, + codeString = it.codeString, + reason = it.reason, + senderKey = it.senderKey + ) + } + } + } + + override fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + SharedSessionEntity.create( + realm = realm, + roomId = roomId, + sessionId = sessionId, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + } + } + + override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult { + return doWithRealm(realmConfiguration) { realm -> + SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let { + IMXCryptoStore.SharedSessionResult(true, it.chainIndex) + } ?: IMXCryptoStore.SharedSessionResult(false, null) + } + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> { + return doWithRealm(realmConfiguration) { realm -> + val result = MXUsersDevicesMap<Int>() + SharedSessionEntity.get(realm, roomId, sessionId) + .groupBy { it.userId } + .forEach { (userId, shared) -> + shared.forEach { + result.setObject(userId, it.deviceId, it.chainIndex) + } + } + + result + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt new file mode 100644 index 0000000000000000000000000000000000000000..fbc1eb6bb1239fe857118eba4768b85c057e2d0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -0,0 +1,458 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields +import org.matrix.android.sdk.internal.di.SerializeNulls +import io.realm.DynamicRealm +import io.realm.RealmMigration +import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 +import timber.log.Timber +import javax.inject.Inject +import org.matrix.androidsdk.crypto.data.MXDeviceInfo as LegacyMXDeviceInfo + +internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration { + + companion object { + // 0, 1, 2: legacy Riot-Android + // 3: migrate to RiotX schema + // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) + const val CRYPTO_STORE_SCHEMA_VERSION = 11L + } + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1Legacy(realm) + if (oldVersion <= 1) migrateTo2Legacy(realm) + if (oldVersion <= 2) migrateTo3RiotX(realm) + if (oldVersion <= 3) migrateTo4(realm) + if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) + if (oldVersion <= 6) migrateTo7(realm) + if (oldVersion <= 7) migrateTo8(realm) + if (oldVersion <= 8) migrateTo9(realm) + if (oldVersion <= 9) migrateTo10(realm) + if (oldVersion <= 10) migrateTo11(realm) + } + + private fun migrateTo1Legacy(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0") + + realm.schema.get("OlmSessionEntity") + ?.addField(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Long::class.java) + ?.transform { + it.setLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, 0) + } + } + + private fun migrateTo2Legacy(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("IncomingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map<String, String>? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("OutgoingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map<String, String>? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Create KeysBackupDataEntity") + + realm.schema.create("KeysBackupDataEntity") + .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) + .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) + .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) + } + + private fun migrateTo3RiotX(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Migrate to RiotX model") + + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) + ?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) + + // Convert format of MXDeviceInfo, package has to be the same. + realm.schema.get("DeviceInfoEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm<LegacyMXDeviceInfo>(oldSerializedData)?.let { legacyMxDeviceInfo -> + val newMxDeviceInfo = MXDeviceInfo( + deviceId = legacyMxDeviceInfo.deviceId, + userId = legacyMxDeviceInfo.userId, + algorithms = legacyMxDeviceInfo.algorithms, + keys = legacyMxDeviceInfo.keys, + signatures = legacyMxDeviceInfo.signatures, + unsigned = legacyMxDeviceInfo.unsigned, + verified = legacyMxDeviceInfo.mVerified + ) + + obj.setString("deviceInfoData", serializeForRealm(newMxDeviceInfo)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + + // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper + realm.schema.get("OlmInboundGroupSessionEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("olmInboundGroupSessionData") + deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 -> + val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier() + val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false) + .apply { + olmInboundGroupSession = mxOlmInboundGroupSession2.mSession + roomId = mxOlmInboundGroupSession2.mRoomId + senderKey = mxOlmInboundGroupSession2.mSenderKey + keysClaimed = mxOlmInboundGroupSession2.mKeysClaimed + forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain + } + + obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + } + + // Version 4L added Cross Signing info persistence + private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") + Timber.d("Create KeyInfoEntity") + + val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity") + .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) + .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + + val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") + .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) + .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) + .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) + + Timber.d("Create CrossSigningInfoEntity") + + val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") + .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) + .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) + .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) + + Timber.d("Updating UserEntity table") + realm.schema.get("UserEntity") + ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) + + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) + + val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) + ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) + ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) + ?.transform { obj -> + + try { + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice -> + + val trustLevel = realm.createObject("TrustLevelEntity") + when (oldDevice.verified) { + MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> { + obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`) + } + MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> { + trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED) + trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) + obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + } + + obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId) + obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey()) + obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms)) + obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys)) + obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) + obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error") + // an unfortunate refactor did modify that class, making deserialization failing + // so we just skip and ignore.. + } + } + ?.removeField("deviceInfoData") + } + + private fun migrateTo5(realm: DynamicRealm) { + Timber.d("Step 4 -> 5") + realm.schema.remove("OutgoingRoomKeyRequestEntity") + realm.schema.remove("IncomingRoomKeyRequestEntity") + + // Not need to migrate existing request, just start fresh? + + realm.schema.create("GossipingEventEntity") + .addField(GossipingEventEntityFields.TYPE, String::class.java) + .addIndex(GossipingEventEntityFields.TYPE) + .addField(GossipingEventEntityFields.CONTENT, String::class.java) + .addField(GossipingEventEntityFields.SENDER, String::class.java) + .addIndex(GossipingEventEntityFields.SENDER) + .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) + .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) + .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) + .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + + realm.schema.create("IncomingGossipingRequestEntity") + .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) + .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) + .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) + .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + + realm.schema.create("OutgoingGossipingRequestEntity") + .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) + .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) + .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + } + + private fun migrateTo6(realm: DynamicRealm) { + Timber.d("Step 5 -> 6") + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java) + } + + private fun migrateTo7(realm: DynamicRealm) { + Timber.d("Step 6 -> 7") + Timber.d("Updating KeyInfoEntity table") + val keyInfoEntities = realm.where("KeyInfoEntity").findAll() + try { + keyInfoEntities.forEach { + val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES) + val objectSignatures: Map<String, Map<String, String>>? = deserializeFromRealm(stringSignatures) + val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures) + it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures) + } + } catch (failure: Throwable) { + } + + // Migrate frozen classes + val inboundGroupSessions = realm.where("OlmInboundGroupSessionEntity").findAll() + inboundGroupSessions.forEach { dynamicObject -> + dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { serializedObject -> + try { + deserializeFromRealm<OlmInboundGroupSessionWrapper?>(serializedObject)?.let { oldFormat -> + val newFormat = oldFormat.exportKeys()?.let { + OlmInboundGroupSessionWrapper2(it) + } + dynamicObject.setString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA, serializeForRealm(newFormat)) + } + } catch (failure: Throwable) { + Timber.e(failure, "## OlmInboundGroupSessionEntity migration failed") + } + } + } + } + + private fun migrateTo8(realm: DynamicRealm) { + Timber.d("Step 7 -> 8") + realm.schema.create("MyDeviceLastSeenInfoEntity") + .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) + .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) + .addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java) + .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true) + + val now = System.currentTimeMillis() + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) + ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) + ?.transform { deviceInfoEntity -> + tryThis { + deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) + } + } + } + + // Fixes duplicate devices in UserEntity#devices + private fun migrateTo9(realm: DynamicRealm) { + Timber.d("Step 8 -> 9") + val userEntities = realm.where("UserEntity").findAll() + userEntities.forEach { + try { + val deviceList = it.getList(UserEntityFields.DEVICES.`$`) + ?: return@forEach + val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) } + if (distinct.size != deviceList.size) { + deviceList.clear() + deviceList.addAll(distinct) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error for migrateTo9") + } + } + } + + // Version 10L added WithHeld Keys Info (MSC2399) + private fun migrateTo10(realm: DynamicRealm) { + Timber.d("Step 9 -> 10") + realm.schema.create("WithHeldSessionEntity") + .addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java) + .addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java) + .addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(WithHeldSessionEntityFields.SESSION_ID) + .addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java) + .addIndex(WithHeldSessionEntityFields.SENDER_KEY) + .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java) + .addField(WithHeldSessionEntityFields.REASON, String::class.java) + + realm.schema.create("SharedSessionEntity") + .addField(SharedSessionEntityFields.ROOM_ID, String::class.java) + .addField(SharedSessionEntityFields.ALGORITHM, String::class.java) + .addField(SharedSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(SharedSessionEntityFields.SESSION_ID) + .addField(SharedSessionEntityFields.USER_ID, String::class.java) + .addIndex(SharedSessionEntityFields.USER_ID) + .addField(SharedSessionEntityFields.DEVICE_ID, String::class.java) + .addIndex(SharedSessionEntityFields.DEVICE_ID) + .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java) + .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true) + } + + // Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity + private fun migrateTo11(realm: DynamicRealm) { + Timber.d("Step 10 -> 11") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1103e69bbc5c3b7d99cfa30ac0eea04c29303c52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import io.realm.annotations.RealmModule + +/** + * Realm module for Crypto store classes + */ +@RealmModule(library = true, + classes = [ + CryptoMetadataEntity::class, + CryptoRoomEntity::class, + DeviceInfoEntity::class, + KeysBackupDataEntity::class, + OlmInboundGroupSessionEntity::class, + OlmSessionEntity::class, + UserEntity::class, + KeyInfoEntity::class, + CrossSigningInfoEntity::class, + TrustLevelEntity::class, + GossipingEventEntity::class, + IncomingGossipingRequestEntity::class, + OutgoingGossipingRequestEntity::class, + MyDeviceLastSeenInfoEntity::class, + WithHeldSessionEntity::class, + SharedSessionEntity::class + ]) +internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt new file mode 100644 index 0000000000000000000000000000000000000000..17538c7cbe44b7211b7295dd4dd69834e6c0cc69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectStreamClass + +/** + * Package has been renamed from `im.vector.matrix.android` to `org.matrix.android.sdk` + * so ensure deserialization of previously stored objects still works + * + * Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes + */ +internal class SafeObjectInputStream(`in`: InputStream) : ObjectInputStream(`in`) { + + init { + enableResolveObject(true) + } + + @Throws(IOException::class, ClassNotFoundException::class) + override fun readClassDescriptor(): ObjectStreamClass { + val read = super.readClassDescriptor() + if (read.name.startsWith("im.vector.matrix.android.")) { + return ObjectStreamClass.lookup(Class.forName(read.name.replace("im.vector.matrix.android.", "org.matrix.android.sdk."))) + } + return read + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..4a303de81cc3e55fe08fb32717c1657488da6e56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.mapper + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import io.realm.RealmList +import timber.log.Timber +import javax.inject.Inject + +internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) { + + private val signaturesAdapter = moshi.adapter<Map<String, Map<String, String>>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + fun update(keyInfo: KeyInfoEntity, cryptoCrossSigningKey: CryptoCrossSigningKey) { + // update signatures? + keyInfo.signatures = serializeSignatures(cryptoCrossSigningKey.signatures) + keyInfo.usages = cryptoCrossSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } + + fun map(userId: String?, keyInfo: KeyInfoEntity?): CryptoCrossSigningKey? { + val pubKey = keyInfo?.publicKeyBase64 ?: return null + return CryptoCrossSigningKey( + userId = userId ?: "", + keys = mapOf("ed25519:$pubKey" to pubKey), + usages = keyInfo.usages.map { it }, + signatures = deserializeSignatures(keyInfo.signatures), + trustLevel = keyInfo.trustLevelEntity?.let { + DeviceTrustLevel( + crossSigningVerified = it.crossSignedVerified ?: false, + locallyVerified = it.locallyVerified ?: false + ) + } + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): KeyInfoEntity { + return KeyInfoEntity().apply { + publicKeyBase64 = keyInfo.unpaddedBase64PublicKey + usages = keyInfo.usages?.let { RealmList(*it.toTypedArray()) } ?: RealmList() + signatures = serializeSignatures(keyInfo.signatures) + // TODO how to handle better, check if same keys? + // reset trust + trustLevelEntity = null + } + } + + fun serializeSignatures(signatures: Map<String, Map<String, String>>?): String { + return signaturesAdapter.toJson(signatures) + } + + fun deserializeSignatures(signatures: String?): Map<String, Map<String, String>>? { + if (signatures == null) { + return null + } + return try { + signaturesAdapter.fromJson(signatures) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..94db368e052b85d3806defff9fd4a508f8d5c4ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.KeyUsage +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CrossSigningInfoEntity( + @PrimaryKey + var userId: String? = null, + var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList() +) : RealmObject() { + + companion object + + fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.MASTER.value) } + + fun setMasterKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.MASTER.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } + + fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + + fun setSelfSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } + + fun getUserSigningKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.USER_SIGNING.value) } + + fun setUserSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.USER_SIGNING.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e3c12f20b3ca4fc199b12267be404b4c08807da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.di.SerializeNulls +import timber.log.Timber + +object CryptoMapper { + + private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + private val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + private val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + private val mapOfStringMigrationAdapter = moshi.adapter<Map<String, Map<String, String>>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity { + return DeviceInfoEntity( + primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId), + userId = deviceInfo.userId, + deviceId = deviceInfo.deviceId, + algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms), + keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys), + signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures), + isBlocked = deviceInfo.isBlocked, + trustLevelEntity = deviceInfo.trustLevel?.let { + TrustLevelEntity( + crossSignedVerified = it.crossSigningVerified, + locallyVerified = it.locallyVerified + ) + }, + // We store the device name if present now + unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName + ) + } + + internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo { + return CryptoDeviceInfo( + userId = deviceInfoEntity.userId ?: "", + deviceId = deviceInfoEntity.deviceId ?: "", + isBlocked = deviceInfoEntity.isBlocked ?: false, + trustLevel = deviceInfoEntity.trustLevelEntity?.let { + DeviceTrustLevel(it.crossSignedVerified ?: false, it.locallyVerified) + }, + unsigned = deviceInfoEntity.unsignedMapJson?.let { UnsignedDeviceInfo(deviceDisplayName = it) }, + signatures = deviceInfoEntity.signatureMapJson?.let { + try { + mapOfStringMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + keys = deviceInfoEntity.keysMapJson?.let { + try { + moshi.adapter<Map<String, String>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )).fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + algorithms = deviceInfoEntity.algorithmListJson?.let { + try { + listMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb79af4747d202aaa51e5efe8a29875cdf40f879 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.olm.OlmAccount + +internal open class CryptoMetadataEntity( + // The current user id. + @PrimaryKey var userId: String? = null, + // The current device id. + var deviceId: String? = null, + // Serialized OlmAccount + var olmAccountData: String? = null, + // The sync token corresponding to the device list. // TODO? + var deviceSyncToken: String? = null, + // Settings for blacklisting unverified devices. + var globalBlacklistUnverifiedDevices: Boolean = false, + // The keys backup version currently used. Null means no backup. + var backupVersion: String? = null, + + // The device keys has been sent to the homeserver + var deviceKeysSentToServer: Boolean = false, + + var xSignMasterPrivateKey: String? = null, + var xSignUserPrivateKey: String? = null, + var xSignSelfSignedPrivateKey: String? = null, + var keyBackupRecoveryKey: String? = null, + var keyBackupRecoveryKeyVersion: String? = null + +// var crossSigningInfoEntity: CrossSigningInfoEntity? = null +) : RealmObject() { + + // Deserialize data + fun getOlmAccount(): OlmAccount? { + return deserializeFromRealm(olmAccountData) + } + + // Serialize data + fun putOlmAccount(olmAccount: OlmAccount?) { + olmAccountData = serializeForRealm(olmAccount) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..e1881e9157b2d751fd0a361cd08daa7f4872728e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CryptoRoomEntity( + @PrimaryKey var roomId: String? = null, + var algorithm: String? = null, + var shouldEncryptForInvitedMembers: Boolean? = null, + var blacklistUnverifiedDevices: Boolean = false) + : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0f4d4954510d5c75738deed6b77e59afa83cb6a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal fun DeviceInfoEntity.Companion.createPrimaryKey(userId: String, deviceId: String) = "$userId|$deviceId" + +internal open class DeviceInfoEntity( + @PrimaryKey var primaryKey: String = "", + var deviceId: String? = null, + var identityKey: String? = null, + var userId: String? = null, + var isBlocked: Boolean? = null, + var algorithmListJson: String? = null, + var keysMapJson: String? = null, + var signatureMapJson: String? = null, + // Will contain the device name from unsigned data if present + var unsignedMapJson: String? = null, + var trustLevelEntity: TrustLevelEntity? = null, + /** + * We use that to make distinction between old devices (there before mine) + * and new ones. Used for example to detect new unverified login + */ + var firstTimeSeenLocalTs: Long? = null +) : RealmObject() { + + @LinkingObjects("devices") + val users: RealmResults<UserEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0a4625826a71e10fd7004e483f408fb0c6eb661 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonDataException +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index +import timber.log.Timber + +/** + * Keep track of gossiping event received in toDevice messages + * (room key request, or sss secret sharing, as well as cancellations) + * + */ +internal open class GossipingEventEntity(@Index var type: String? = "", + var content: String? = null, + @Index var sender: String? = null, + var decryptionResultJson: String? = null, + var decryptionErrorCode: String? = null, + var ageLocalTs: Long? = null) : RealmObject() { + + private var sendStateStr: String = SendState.UNKNOWN.name + + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + + companion object + + fun setDecryptionResult(result: MXEventDecryptionResult) { + val decryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) + decryptionErrorCode = null + } + + fun toModel(): Event { + return Event( + type = this.type ?: "", + content = ContentMapper.map(this.content), + senderId = this.sender + ).also { + it.ageLocalTs = this.ageLocalTs + it.sendState = this.sendState + this.decryptionResultJson?.let { json -> + try { + it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse decryption result") + } + } + // TODO get the full crypto error object + it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> + MXCryptoError.ErrorType.valueOf(errorCode) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c15df27874ac1ae5881d6f7b6a96c61c00151cda --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", + @Index var typeStr: String? = null, + var otherUserId: String? = null, + var requestedInfoStr: String? = null, + var otherDeviceId: String? = null, + var localCreationTimestamp: Long? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = GossipingRequestState.NONE.name + + var requestState: GossipingRequestState + get() { + return tryThis { GossipingRequestState.valueOf(requestStateStr) } + ?: GossipingRequestState.NONE + } + set(value) { + requestStateStr = value.name + } + + companion object + + fun toIncomingGossipingRequest(): IncomingShareRequestCommon { + return when (type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + state = requestState, + localCreationTimestamp = localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + secretName = getRequestedSecretName(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + localCreationTimestamp = localCreationTimestamp + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..125fbd8118dae91529b0611a39b9d9f2deb0a541 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class KeyInfoEntity( + var publicKeyBase64: String? = null, +// var isTrusted: Boolean = false, + var usages: RealmList<String> = RealmList(), + /** + * The signature of this MXDeviceInfo. + * A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>" + */ + var signatures: String? = null, + var trustLevelEntity: TrustLevelEntity? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..0155ed9ccef218151c0b3d8e09252c6305f601c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class KeysBackupDataEntity( + // Primary key to update this object. There is only one object, so it's a constant, please do not set it + @PrimaryKey + var primaryKey: Int = 0, + // The last known hash of the backed up keys on the server + var backupLastServerHash: String? = null, + // The last known number of backed up keys on the server + var backupLastServerNumberOfKeys: Int? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..64b04827d60c2a7bde8f9692937f03d28c5ca6ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class MyDeviceLastSeenInfoEntity( + /**The device id*/ + @PrimaryKey var deviceId: String? = null, + /** The device display name*/ + var displayName: String? = null, + /** The last time this device has been seen. */ + var lastSeenTs: Long? = null, + /** The last ip address*/ + var lastSeenIp: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d20b7582df732f3bc1c43b6c9bc7f8d04a1f9ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import timber.log.Timber + +internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" + +internal open class OlmInboundGroupSessionEntity( + // Combined value to build a primary key + @PrimaryKey var primaryKey: String? = null, + var sessionId: String? = null, + var senderKey: String? = null, + // olmInboundGroupSessionData contains Json + var olmInboundGroupSessionData: String? = null, + // Indicate if the key has been backed up to the homeserver + var backedUp: Boolean = false) + : RealmObject() { + + fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { + return try { + deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData) + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } + } + + fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { + olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..f804a64182e2637efc2a4bd42903d831bdd7bed0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.olm.OlmSession + +internal fun OlmSessionEntity.Companion.createPrimaryKey(sessionId: String, deviceKey: String) = "$sessionId|$deviceKey" + +// olmSessionData is a serialized OlmSession +internal open class OlmSessionEntity(@PrimaryKey var primaryKey: String = "", + var sessionId: String? = null, + var deviceKey: String? = null, + var olmSessionData: String? = null, + var lastReceivedMessageTs: Long = 0) + : RealmObject() { + + fun getOlmSession(): OlmSession? { + return deserializeFromRealm(olmSessionData) + } + + fun putOlmSession(olmSession: OlmSession?) { + olmSessionData = serializeForRealm(olmSession) + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..2880735d6bc3054ae61b9810390f3c20d7eb16ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class OutgoingGossipingRequestEntity( + @Index var requestId: String? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + @Index var typeStr: String? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name + + var requestState: OutgoingGossipingRequestState + get() { + return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } + ?: OutgoingGossipingRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + companion object { + + private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> = + MoshiProvider + .providesMoshi() + .adapter<Map<String, List<String>>>( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + fun toOutgoingGossipingRequest(): OutgoingGossipingRequest { + return when (type) { + GossipRequestType.KEY -> { + OutgoingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + state = requestState + ) + } + GossipRequestType.SECRET -> { + OutgoingSecretRequest( + secretName = getRequestedSecretName(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + state = requestState + ) + } + } + } + + private fun getRecipients(): Map<String, List<String>>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map<String, List<String>>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa647d02c1eba0ea10a3efc3beae867c5129d093 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Keep a record of to whom (user/device) a given session should have been shared. + * It will be used to reply to keyshare requests from other users, in order to see if + * this session was originaly shared with a given user + */ +internal open class SharedSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var userId: String? = null, + @Index var deviceId: String? = null, + var chainIndex: Int? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb2933e3c4ef4b79c68af52b3e0db6e42cee3782 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject + +internal open class TrustLevelEntity( + var crossSignedVerified: Boolean? = null, + var locallyVerified: Boolean? = null +) : RealmObject() { + + companion object + + fun isVerified(): Boolean = crossSignedVerified == true || locallyVerified == true +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..2820f72ef439b4032eb3e54ede53820b9a344f0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class UserEntity( + @PrimaryKey var userId: String? = null, + var devices: RealmList<DeviceInfoEntity> = RealmList(), + var crossSigningInfoEntity: CrossSigningInfoEntity? = null, + var deviceTrackingStatus: Int = 0) + : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..36ffe85183e14215edd2f3f3d1dd70c541d6f2c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room. + * Sometimes this may be inadvertent (for example, if the sending device is not aware of some devices that have joined), + * but some times, this may be purposeful. + * For example, the sender may have blacklisted certain devices or users, + * or may be choosing to not send the megolm key to devices that they have not verified yet. + */ +internal open class WithHeldSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var senderKey: String? = null, + var codeString: String? = null, + var reason: String? = null +) : RealmObject() { + + var code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } + set(code) { + codeString = code?.value + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..58644550278218e0ca130d9f8f348a4400c68c16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity { + return realm.where<CrossSigningInfoEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: realm.createObject(userId) +} + +internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? { + return realm.where<CrossSigningInfoEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..f65b1a3c717d6162e0023812802d7cbaa68116dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a room + */ +internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { + return getById(realm, roomId) ?: realm.createObject(roomId) +} + +/** + * Get a room + */ +internal fun CryptoRoomEntity.Companion.getById(realm: Realm, roomId: String): CryptoRoomEntity? { + return realm.where<CryptoRoomEntity>() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0e677e078c2af8a3c505a5b6e6c3dd776717103 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a device info + */ +internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { + val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId) + + return realm.where<DeviceInfoEntity>() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key) + .findFirst() + ?: realm.createObject<DeviceInfoEntity>(key) + .apply { + this.deviceId = deviceId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..885cadb5e552686e539aad41f7d778f46829e1aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields +import io.realm.Realm +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String) + : SharedSessionEntity? { + return realm.where<SharedSessionEntity>() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .equalTo(SharedSessionEntityFields.USER_ID, userId) + .equalTo(SharedSessionEntityFields.DEVICE_ID, deviceId) + .findFirst() +} + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String) + : RealmResults<SharedSessionEntity> { + return realm.where<SharedSessionEntity>() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findAll() +} + +internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + : SharedSessionEntity { + return realm.createObject<SharedSessionEntity>().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + this.userId = userId + this.deviceId = deviceId + this.chainIndex = chainIndex + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..e64dcb815dd376ca40cdd22c7a891467928f4343 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a user + */ +internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): UserEntity { + return realm.where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: realm.createObject(userId) +} + +/** + * Delete a user + */ +internal fun UserEntity.Companion.delete(realm: Realm, userId: String) { + realm.where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3a5560dd45305daa03bd88f454ea792e220f522 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return realm.where<WithHeldSessionEntity>() + .equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId) + .equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId) + .equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findFirst() +} + +internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return get(realm, roomId, sessionId) + ?: realm.createObject<WithHeldSessionEntity>().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5ee6aa9bf1c97365314a4048adad4e9f7ba175c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface ClaimOneTimeKeysForUsersDeviceTask : Task<ClaimOneTimeKeysForUsersDeviceTask.Params, MXUsersDevicesMap<MXKey>> { + data class Params( + // a list of users, devices and key types to retrieve keys for. + val usersDevicesKeyTypesMap: MXUsersDevicesMap<String> + ) +} + +internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : ClaimOneTimeKeysForUsersDeviceTask { + + override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): MXUsersDevicesMap<MXKey> { + val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) + + val keysClaimResponse = executeRequest<KeysClaimResponse>(eventBus) { + apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) + } + val map = MXUsersDevicesMap<MXKey>() + keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> + for ((userId, mapByUserId) in oneTimeKeys) { + for ((deviceId, deviceKey) in mapByUserId) { + val mxKey = MXKey.from(deviceKey) + + if (mxKey != null) { + map.setObject(userId, deviceId, mxKey) + } else { + Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") + } + } + } + } + return map + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f0d9eaaf97147b952cf91b4c80544031d31c665 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> { + data class Params( + val deviceId: String + ) +} + +internal class DefaultDeleteDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : DeleteDeviceTask { + + override suspend fun execute(params: DeleteDeviceTask.Params) { + try { + executeRequest<Unit>(eventBus) { + apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) + } + } catch (throwable: Throwable) { + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f67ec666db8b4c8a826d95447b2d381084e8229 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteDeviceWithUserPasswordTask : Task<DeleteDeviceWithUserPasswordTask.Params, Unit> { + data class Params( + val deviceId: String, + val authSession: String?, + val password: String + ) +} + +internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( + private val cryptoApi: CryptoApi, + @UserId private val userId: String, + private val eventBus: EventBus +) : DeleteDeviceWithUserPasswordTask { + + override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params) { + return executeRequest(eventBus) { + apiCall = cryptoApi.deleteDevice(params.deviceId, + DeleteDeviceParams( + userPasswordAuth = UserPasswordAuth( + type = LoginFlowTypes.PASSWORD, + session = params.authSession, + user = userId, + password = params.password + ) + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0539005987cfd1e6c9a7c1d4e9a60b9a3c9292d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> { + data class Params( + // the list of users to get keys for. + val userIds: List<String>, + // the up-to token + val token: String? + ) +} + +internal class DefaultDownloadKeysForUsers @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : DownloadKeysForUsersTask { + + override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { + val downloadQuery = params.userIds.associateWith { emptyList<String>() } + + val body = KeysQueryBody( + deviceKeys = downloadQuery, + token = params.token?.takeIf { it.isNotEmpty() } + ) + + return executeRequest(eventBus) { + apiCall = cryptoApi.downloadKeysForUsers(body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e0a85d50c013ed2948aaa1e10e3e5d8b3d7b146f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitCallback +import javax.inject.Inject + +internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> { + data class Params(val roomId: String, + val event: Event, + /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + val keepKeys: List<String>? = null, + val crypto: CryptoService + ) +} + +internal class DefaultEncryptEventTask @Inject constructor( +// private val crypto: CryptoService + private val localEchoRepository: LocalEchoRepository +) : EncryptEventTask { + override suspend fun execute(params: EncryptEventTask.Params): Event { + if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event + val localEvent = params.event + if (localEvent.eventId == null) { + throw IllegalArgumentException() + } + + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + +// try { + awaitCallback<MXEncryptEventContentResult> { + params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + }.let { result -> + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + return localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + } +// } catch (throwable: Throwable) { +// val sendState = when (throwable) { +// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES +// else -> SendState.UNDELIVERED +// } +// localEchoUpdater.updateSendState(localEvent.eventId, sendState) +// throw throwable +// } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e1db5e0c982950b108008938148adf3b1a7e2dbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetDeviceInfoTask : Task<GetDeviceInfoTask.Params, DeviceInfo> { + data class Params(val deviceId: String) +} + +internal class DefaultGetDeviceInfoTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDeviceInfoTask { + + override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo { + return executeRequest(eventBus) { + apiCall = cryptoApi.getDeviceInfo(params.deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea8be725f0b0fa9864e113deee3331d08caff527 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetDevicesTask : Task<Unit, DevicesListResponse> + +internal class DefaultGetDevicesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDevicesTask { + + override suspend fun execute(params: Unit): DevicesListResponse { + return executeRequest(eventBus) { + apiCall = cryptoApi.getDevices() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..57a4881a5180680531921b1e4e30680cf1fa4501 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeyChangesTask : Task<GetKeyChangesTask.Params, KeyChangesResponse> { + data class Params( + // the start token. + val from: String, + // the up-to token. + val to: String + ) +} + +internal class DefaultGetKeyChangesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetKeyChangesTask { + + override suspend fun execute(params: GetKeyChangesTask.Params): KeyChangesResponse { + return executeRequest(eventBus) { + apiCall = cryptoApi.getKeyChanges(params.from, params.to) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2c7e87b67d37b6aee8f19ff1d68faf972ef9e79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import dagger.Lazy +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder +import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.KeyUsage +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmPkSigning +import timber.log.Timber +import javax.inject.Inject + +internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> { + data class Params( + val authParams: UserPasswordAuth? + ) + + data class Result( + val masterKeyPK: String, + val userKeyPK: String, + val selfSigningKeyPK: String, + val masterKeyInfo: CryptoCrossSigningKey, + val userKeyInfo: CryptoCrossSigningKey, + val selfSignedKeyInfo: CryptoCrossSigningKey + ) +} + +internal class DefaultInitializeCrossSigningTask @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, + private val uploadSigningKeysTask: UploadSigningKeysTask, + private val uploadSignaturesTask: UploadSignaturesTask +) : InitializeCrossSigningTask { + + override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result { + var masterPkOlm: OlmPkSigning? = null + var userSigningPkOlm: OlmPkSigning? = null + var selfSigningPkOlm: OlmPkSigning? = null + + try { + // ================= + // MASTER KEY + // ================= + + masterPkOlm = OlmPkSigning() + val masterKeyPrivateKey = OlmPkSigning.generateSeed() + val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) + + Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") + + // ================= + // USER KEY + // ================= + userSigningPkOlm = OlmPkSigning() + val uskPrivateKey = OlmPkSigning.generateSeed() + val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) + + Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") + + // Sign userSigningKey with master + val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // ================= + // SELF SIGNING KEY + // ================= + selfSigningPkOlm = OlmPkSigning() + val sskPrivateKey = OlmPkSigning.generateSeed() + val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) + + Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") + + // Sign userSigningKey with master + val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // I need to upload the keys + val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) + .key(masterPublicKey) + .build() + val uploadSigningKeysParams = UploadSigningKeysTask.Params( + masterKey = mskCrossSigningKeyInfo, + userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .signature(userId, masterPublicKey, signedUSK) + .build(), + selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .signature(userId, masterPublicKey, signedSSK) + .build(), + userPasswordAuth = params.authParams + ) + + uploadSigningKeysTask.execute(uploadSigningKeysParams) + + // Sign the current device with SSK + val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() + + val myDevice = myDeviceInfoHolder.get().myDevice + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) + val signedDevice = selfSigningPkOlm.sign(canonicalJson) + val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) + .also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) + } + myDevice.copy(signatures = updateSignatures).let { + uploadSignatureQueryBuilder.withDeviceInfo(it) + } + + // sign MSK with device key (migration) and upload signatures + val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) + olmDevice.signMessage(message)?.let { sign -> + val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() + ?: HashMap()).also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) + } + mskCrossSigningKeyInfo.copy( + signatures = mskUpdatedSignatures + ).let { + uploadSignatureQueryBuilder.withSigningKeyInfo(it) + } + } + + // TODO should we ignore failure of that? + uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) + + return InitializeCrossSigningTask.Result( + masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(), + userKeyPK = uskPrivateKey.toBase64NoPadding(), + selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(), + masterKeyInfo = uploadSigningKeysParams.masterKey, + userKeyInfo = uploadSigningKeysParams.userKey, + selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey + ) + } finally { + masterPkOlm?.releaseSigning() + userSigningPkOlm?.releaseSigning() + selfSigningPkOlm?.releaseSigning() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..870980bde22177f50a4c5ac172fd6ee801bea7aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendEventTask : Task<SendEventTask.Params, String> { + data class Params( + val event: Event, + val cryptoService: CryptoService? + ) +} + +internal class DefaultSendEventTask @Inject constructor( + private val localEchoRepository: LocalEchoRepository, + private val encryptEventTask: DefaultEncryptEventTask, + private val roomAPI: RoomAPI, + private val eventBus: EventBus) : SendEventTask { + + override suspend fun execute(params: SendEventTask.Params): String { + val event = handleEncryption(params) + val localId = event.eventId!! + + try { + localEchoRepository.updateSendState(localId, SendState.SENDING) + val executeRequest = executeRequest<SendResponse>(eventBus) { + apiCall = roomAPI.send( + localId, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type + ) + } + localEchoRepository.updateSendState(localId, SendState.SENT) + return executeRequest.eventId + } catch (e: Throwable) { + localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun handleEncryption(params: SendEventTask.Params): Event { + if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return params.event + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..20153ef460057d45e9d028b28adbe0bbff751fd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject +import kotlin.random.Random + +internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> { + data class Params( + // the type of event to send + val eventType: String, + // the content to send. Map from user_id to device_id to content dictionary. + val contentMap: MXUsersDevicesMap<Any>, + // the transactionId + val transactionId: String? = null + ) +} + +internal class DefaultSendToDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SendToDeviceTask { + + override suspend fun execute(params: SendToDeviceTask.Params) { + val sendToDeviceBody = SendToDeviceBody( + messages = params.contentMap.map + ) + + return executeRequest(eventBus) { + apiCall = cryptoApi.sendToDevice( + params.eventType, + params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), + sendToDeviceBody + ) + isRetryable = true + maxRetryCount = 3 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..09baf88e5904b897fe16c819668b1b4fc7ede74a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> { + data class Params( + val event: Event, + val cryptoService: CryptoService? + ) +} + +internal class DefaultSendVerificationMessageTask @Inject constructor( + private val localEchoRepository: LocalEchoRepository, + private val encryptEventTask: DefaultEncryptEventTask, + private val roomAPI: RoomAPI, + private val eventBus: EventBus) : SendVerificationMessageTask { + + override suspend fun execute(params: SendVerificationMessageTask.Params): String { + val event = handleEncryption(params) + val localId = event.eventId!! + + try { + localEchoRepository.updateSendState(localId, SendState.SENDING) + val executeRequest = executeRequest<SendResponse>(eventBus) { + apiCall = roomAPI.send( + localId, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type + ) + } + localEchoRepository.updateSendState(localId, SendState.SENT) + return executeRequest.eventId + } catch (e: Throwable) { + localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { + if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return params.event + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3900550c5e58ed172a0bbd4ff0e8f4b5086e60d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SetDeviceNameTask : Task<SetDeviceNameTask.Params, Unit> { + data class Params( + // the device id + val deviceId: String, + // the device name + val deviceName: String + ) +} + +internal class DefaultSetDeviceNameTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SetDeviceNameTask { + + override suspend fun execute(params: SetDeviceNameTask.Params) { + val body = UpdateDeviceInfoBody( + displayName = params.deviceName + ) + return executeRequest(eventBus) { + apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b41dcf6dd02eaedefd285659574d951dfd94b879 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface UploadKeysTask : Task<UploadKeysTask.Params, KeysUploadResponse> { + data class Params( + // the device keys to send. + val deviceKeys: DeviceKeys?, + // the one-time keys to send. + val oneTimeKeys: JsonDict? + ) +} + +internal class DefaultUploadKeysTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadKeysTask { + + override suspend fun execute(params: UploadKeysTask.Params): KeysUploadResponse { + val body = KeysUploadBody( + deviceKeys = params.deviceKeys, + oneTimeKeys = params.oneTimeKeys + ) + + Timber.i("## Uploading device keys -> $body") + + return executeRequest(eventBus) { + apiCall = cryptoApi.uploadKeys(body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..255d06ea7c81dcdf4799509edd70647d277f655b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSignaturesTask : Task<UploadSignaturesTask.Params, Unit> { + data class Params( + val signatures: Map<String, Map<String, Any>> + ) +} + +internal class DefaultUploadSignaturesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSignaturesTask { + + override suspend fun execute(params: UploadSignaturesTask.Params) { + try { + val response = executeRequest<SignatureUploadResponse>(eventBus) { + this.isRetryable = true + this.maxRetryCount = 10 + this.apiCall = cryptoApi.uploadSignatures(params.signatures) + } + if (response.failures?.isNotEmpty() == true) { + throw Throwable(response.failures.toString()) + } + return + } catch (f: Failure) { + throw f + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..c7844fbfe4e8009d63894c55b7a88dffb0c95685 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.model.toRest +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Unit> { + data class Params( + // the MSK + val masterKey: CryptoCrossSigningKey, + // the USK + val userKey: CryptoCrossSigningKey, + // the SSK + val selfSignedKey: CryptoCrossSigningKey, + /** + * - If null: + * - no retry will be performed + * - If not null, it may or may not contain a sessionId: + * - If sessionId is null: + * - password should not be null: the task will perform a first request to get a sessionId, and then a second one + * - If sessionId is not null: + * - password should not be null as well, and no retry will be performed + */ + val userPasswordAuth: UserPasswordAuth? + ) +} + +data class UploadSigningKeys(val failures: Map<String, Any>?) : Failure.FeatureFailure() + +internal class DefaultUploadSigningKeysTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSigningKeysTask { + + override suspend fun execute(params: UploadSigningKeysTask.Params) { + val paramsHaveSessionId = params.userPasswordAuth?.session != null + + val uploadQuery = UploadSigningKeysBody( + masterKey = params.masterKey.toRest(), + userSigningKey = params.userKey.toRest(), + selfSigningKey = params.selfSignedKey.toRest(), + // If sessionId is provided, use the userPasswordAuth + auth = params.userPasswordAuth.takeIf { paramsHaveSessionId } + ) + try { + doRequest(uploadQuery) + } catch (throwable: Throwable) { + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + if (registrationFlowResponse != null + && registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } + && params.userPasswordAuth?.password != null + && !paramsHaveSessionId + ) { + // Retry with authentication + doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))) + } else { + // Other error + throw throwable + } + } + } + + private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { + val keysQueryResponse = executeRequest<KeysQueryResponse>(eventBus) { + apiCall = cryptoApi.uploadSigningKeys(uploadQuery) + } + if (keysQueryResponse.failures?.isNotEmpty() == true) { + throw UploadSigningKeys(keysQueryResponse.failures) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt new file mode 100644 index 0000000000000000000000000000000000000000..f93dc7126ad7fff4b4a3ec203e86e19bb69987ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.tools + +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.ceil + +/** + * HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256) + * [RFC-5869] https://tools.ietf.org/html/rfc5869 + */ +object HkdfSha256 { + + fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray { + return expand(extract(salt, inputKeyMaterial), info, outputLength) + } + + /** + * HkdfSha256-Extract(salt, IKM) -> PRK + * + * @param salt optional salt value (a non-secret random value); + * if not provided, it is set to a string of HashLen (size in octets) zeros. + * @param ikm input keying material + */ + private fun extract(salt: ByteArray?, ikm: ByteArray): ByteArray { + val mac = initMac(salt ?: ByteArray(HASH_LEN) { 0.toByte() }) + return mac.doFinal(ikm) + } + + /** + * HkdfSha256-Expand(PRK, info, L) -> OKM + * + * @param prk a pseudorandom key of at least HashLen bytes (usually, the output from the extract step) + * @param info optional context and application specific information (can be empty) + * @param outputLength length of output keying material in bytes (<= 255*HashLen) + * @return OKM output keying material + */ + private fun expand(prk: ByteArray, info: ByteArray = ByteArray(0), outputLength: Int): ByteArray { + require(outputLength <= 255 * HASH_LEN) { "outputLength must be less than or equal to 255*HashLen" } + + /* + The output OKM is calculated as follows: + Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; + + + N = ceil(L/HashLen) + T = T(1) | T(2) | T(3) | ... | T(N) + OKM = first L octets of T + + where: + T(0) = empty string (zero length) + T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + ... + */ + val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt() + + var stepHash = ByteArray(0) // T(0) empty string (zero length) + + val generatedBytes = ByteArrayOutputStream() // ByteBuffer.allocate(Math.multiplyExact(n, HASH_LEN)) + val mac = initMac(prk) + for (roundNum in 1..n) { + mac.reset() + val t = ByteBuffer.allocate(stepHash.size + info.size + 1).apply { + put(stepHash) + put(info) + put(roundNum.toByte()) + } + stepHash = mac.doFinal(t.array()) + generatedBytes.write(stepHash) + } + + return generatedBytes.toByteArray().sliceArray(0 until outputLength) + } + + private fun initMac(secret: ByteArray): Mac { + val mac = Mac.getInstance(HASH_ALG) + mac.init(SecretKeySpec(secret, HASH_ALG)) + return mac + } + + private const val HASH_LEN = 32 + private const val HASH_ALG = "HmacSHA256" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bd9e1282f0c10b9580642fe762deb6f1b7d6d84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.tools + +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility + +fun <T> withOlmEncryption(block: (OlmPkEncryption) -> T): T { + val olmPkEncryption = OlmPkEncryption() + try { + return block(olmPkEncryption) + } finally { + olmPkEncryption.releaseEncryption() + } +} + +fun <T> withOlmDecryption(block: (OlmPkDecryption) -> T): T { + val olmPkDecryption = OlmPkDecryption() + try { + return block(olmPkDecryption) + } finally { + olmPkDecryption.releaseDecryption() + } +} + +fun <T> withOlmSigning(block: (OlmPkSigning) -> T): T { + val olmPkSigning = OlmPkSigning() + try { + return block(olmPkSigning) + } finally { + olmPkSigning.releaseSigning() + } +} + +fun <T> withOlmUtility(block: (OlmUtility) -> T): T { + val olmUtility = OlmUtility() + try { + return block(olmUtility) + } finally { + olmUtility.releaseUtility() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..009979db49b85707c18027e25e9fc9f31d85af22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import android.util.Base64 +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber + +internal class DefaultIncomingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val userId: String, + override val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + deviceFingerprint: String, + transactionId: String, + otherUserID: String, + private val autoAccept: Boolean = false +) : SASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + deviceFingerprint, + transactionId, + otherUserID, + null, + isIncoming = true), + IncomingSasVerificationTransaction { + + override val uxState: IncomingSasVerificationTransaction.UxState + get() { + return when (val immutableState = state) { + is VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT + is VerificationTxState.SendingAccept, + is VerificationTxState.Accepted, + is VerificationTxState.OnKeyReceived, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } else { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } + } + else -> IncomingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { + Timber.v("## SAS I: received verification request from state $state") + if (state != VerificationTxState.None) { + Timber.e("## SAS I: received verification request from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + this.startReq = startReq + state = VerificationTxState.OnStarted + this.otherDeviceId = startReq.fromDevice + + if (autoAccept) { + performAccept() + } + } + + override fun performAccept() { + if (state != VerificationTxState.OnStarted) { + Timber.e("## SAS Cannot perform accept from state $state") + return + } + + // Select a key agreement protocol, a hash algorithm, a message authentication code, + // and short authentication string methods out of the lists given in requester's message. + val agreedProtocol = startReq!!.keyAgreementProtocols.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) } + val agreedHash = startReq!!.hashes.firstOrNull { KNOWN_HASHES.contains(it) } + val agreedMac = startReq!!.messageAuthenticationCodes.firstOrNull { KNOWN_MACS.contains(it) } + val agreedShortCode = startReq!!.shortAuthenticationStrings.filter { KNOWN_SHORT_CODES.contains(it) } + + // No common key sharing/hashing/hmac/SAS methods. + // If a device is unable to complete the verification because the devices are unable to find a common key sharing, + // hashing, hmac, or SAS method, then it should send a m.key.verification.cancel message + if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } + || agreedShortCode.isNullOrEmpty()) { + // Failed to find agreement + Timber.e("## SAS Failed to find agreement ") + cancel(CancelCode.UnknownMethod) + return + } + + // Bob’s device ensures that it has a copy of Alice’s device key. + val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!) + + if (mxDeviceInfo?.fingerprint() == null) { + Timber.e("## SAS Failed to find device key ") + // TODO force download keys!! + // would be probably better to download the keys + // for now I cancel + cancel(CancelCode.User) + } else { + // val otherKey = info.identityKey() + // need to jump back to correct thread + val accept = transport.createAccept( + tid = transactionId, + keyAgreementProtocol = agreedProtocol!!, + hash = agreedHash!!, + messageAuthenticationCode = agreedMac!!, + shortAuthenticationStrings = agreedShortCode, + commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT) + ) + doAccept(accept) + } + } + + private fun doAccept(accept: VerificationInfoAccept) { + this.accepted = accept.asValidObject() + Timber.v("## SAS incoming accept request id:$transactionId") + + // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, + // concatenated with the canonical JSON representation of the content of the m.key.verification.start message + val concat = getSAS().publicKey + startReq!!.canonicalJson + accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" + // we need to send this to other device now + state = VerificationTxState.SendingAccept + sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, VerificationTxState.Accepted, CancelCode.User) { + if (state == VerificationTxState.SendingAccept) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.Accepted + } + } + } + + override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { + Timber.v("## SAS invalid message for incoming request id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { + Timber.v("## SAS received key for request id:$transactionId") + if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { + Timber.e("## SAS received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Alice’s device, + // Bob’s device replies with a to_device message with type set to m.key.verification.key, + // sending Bob’s public key QB + val pubKey = getSAS().publicKey + + val keyToDevice = transport.createKey(transactionId, pubKey) + // we need to send this to other device now + state = VerificationTxState.SendingKey + this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { + if (state == VerificationTxState.SendingKey) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.KeySent + } + } + + // Alice’s and Bob’s devices perform an Elliptic-curve Diffie-Hellman + // (calculate the point (x,y)=dAQB=dBQA and use x as the result of the ECDH), + // using the result as the shared secret. + + getSAS().setTheirPublicKey(otherKey) + + shortCodeBytes = calculateSASBytes() + + if (BuildConfig.LOG_PRIVATE_DATA) { + Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") + Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + } + + state = VerificationTxState.ShortCodeReady + } + + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SASâ€, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { + Timber.v("## SAS I: received mac for request id:$transactionId") + // Check for state? + if (state != VerificationTxState.SendingKey + && state != VerificationTxState.KeySent + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { + Timber.e("## SAS I: received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + theirMac = vMac + + // Do I have my Mac? + if (myMac != null) { + // I can check + verifyMacs(vMac) + } + // Wait for ShortCode Accepted + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..07e98f52b7ea91046c29cf04861fcbd5069b737d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber + +internal class DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + userId: String, + deviceId: String?, + cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String +) : SASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + deviceFingerprint, + transactionId, + otherUserId, + otherDeviceId, + isIncoming = false), + OutgoingSasVerificationTransaction { + + override val uxState: OutgoingSasVerificationTransaction.UxState + get() { + return when (val immutableState = state) { + is VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START + is VerificationTxState.SendingStart, + is VerificationTxState.Started, + is VerificationTxState.OnAccepted, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent, + is VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } else { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } + } + else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { + Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + fun start() { + if (state != VerificationTxState.None) { + Timber.e("## SAS O: start verification from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + val startMessage = transport.createStartForSas( + deviceId ?: "", + transactionId, + KNOWN_AGREEMENT_PROTOCOLS, + KNOWN_HASHES, + KNOWN_MACS, + KNOWN_SHORT_CODES + ) + + startReq = startMessage.asValidObject() as? ValidVerificationInfoStart.SasVerificationInfoStart + state = VerificationTxState.SendingStart + + sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.Started, + CancelCode.User, + null + ) + } + +// fun request() { +// if (state != VerificationTxState.None) { +// Timber.e("## start verification from invalid state") +// // should I cancel?? +// throw IllegalStateException("Interactive Key verification already started") +// } +// +// val requestMessage = KeyVerificationRequest( +// fromDevice = session.sessionParams.deviceId ?: "", +// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), +// timestamp = System.currentTimeMillis().toInt(), +// transactionId = transactionId +// ) +// +// sendToOther( +// EventType.KEY_VERIFICATION_REQUEST, +// requestMessage, +// VerificationTxState.None, +// CancelCode.User, +// null +// ) +// } + + override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { + Timber.v("## SAS O: onVerificationAccept id:$transactionId") + if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) { + Timber.e("## SAS O: received accept request from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + // Check that the agreement is correct + if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) + || !KNOWN_HASHES.contains(accept.hash) + || !KNOWN_MACS.contains(accept.messageAuthenticationCode) + || accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { + Timber.e("## SAS O: received invalid accept") + cancel(CancelCode.UnknownMethod) + return + } + + // Upon receipt of the m.key.verification.accept message from Bob’s device, + // Alice’s device stores the commitment value for later use. + accepted = accept + state = VerificationTxState.OnAccepted + + // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), + // and replies with a to_device message with type set to “m.key.verification.keyâ€, sending Alice’s public key QA + val pubKey = getSAS().publicKey + + val keyToDevice = transport.createKey(transactionId, pubKey) + // we need to send this to other device now + state = VerificationTxState.SendingKey + sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + if (state == VerificationTxState.SendingKey) { + state = VerificationTxState.KeySent + } + } + } + + override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { + Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") + if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Bob’s device, + // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept + // message is the same as the expected value based on the value of the key property received + // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. + + // check commitment + val concat = vKey.key + startReq!!.canonicalJson + val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" + + if (accepted!!.commitment.equals(otherCommitment)) { + getSAS().setTheirPublicKey(otherKey) + shortCodeBytes = calculateSASBytes() + state = VerificationTxState.ShortCodeReady + } else { + // bad commitment + cancel(CancelCode.MismatchedCommitment) + } + } + + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SASâ€, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { + Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") + // There is starting to be a huge amount of state / race here :/ + if (state != VerificationTxState.OnKeyReceived + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.KeySent + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { + Timber.e("## SAS O: received mac from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + theirMac = vMac + + // Do I have my Mac? + if (myMac != null) { + // I can check + verifyMacs(vMac) + } + // Wait for ShortCode Accepted + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..e61497fd317a5811650528d2b6d0c7ce69126b5b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -0,0 +1,1479 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import android.os.Handler +import android.os.Looper +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +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 +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +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.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady +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.api.session.crypto.verification.safeValueOf +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction +import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData +import org.matrix.android.sdk.internal.crypto.verification.qrcode.generateSharedSecretV2 +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import kotlin.collections.set + +@SessionScope +internal class DefaultVerificationService @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, + private val deviceListManager: DeviceListManager, + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, + private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, + private val crossSigningService: CrossSigningService, + private val cryptoCoroutineScope: CoroutineScope, + private val taskExecutor: TaskExecutor +) : DefaultVerificationTransaction.Listener, VerificationService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + // Cannot be injected in constructor as it creates a dependency cycle + lateinit var cryptoService: CryptoService + + // map [sender : [transaction]] + private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>() + + // we need to keep track of finished transaction + // It will be used for gossiping (to send request after request is completed and 'done' by other) + private val pastTransactions = HashMap<String, HashMap<String, DefaultVerificationTransaction>>() + + /** + * Map [sender: [PendingVerificationRequest]] + * For now we keep all requests (even terminated ones) during the lifetime of the app. + */ + private val pendingRequests = HashMap<String, MutableList<PendingVerificationRequest>>() + + // Event received from the sync + fun onToDeviceEvent(event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + onCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onKeyReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onReadyReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + onDoneReceived(event) + } + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + onRequestReceived(event) + } + else -> { + // ignore + } + } + } + } + + fun onRoomEvent(event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onRoomStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device + onRoomCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onRoomAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onRoomKeyRequestReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onRoomMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onRoomReadyReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + onRoomDoneReceived(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) { + onRoomRequestReceived(event) + } + } + else -> { + // ignore + } + } + } + } + + private var listeners = ArrayList<VerificationService.Listener>() + + override fun addListener(listener: VerificationService.Listener) { + uiHandler.post { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + } + + override fun removeListener(listener: VerificationService.Listener) { + uiHandler.post { + listeners.remove(listener) + } + } + + private fun dispatchTxAdded(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchTxUpdated(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + userId, + deviceID) + + listeners.forEach { + try { + it.markedAsManuallyVerified(userId, deviceID) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + fun onRoomRequestHandledByOtherDevice(event: Event) { + val requestInfo = event.content.toModel<MessageRelationContent>() + ?: return + val requestId = requestInfo.relatesTo?.eventId ?: return + getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { + updatePendingRequest( + it.copy( + handledByOtherSession = true + ) + ) + } + } + + private fun onRequestReceived(event: Event) { + val validRequestInfo = event.getClearContent().toModel<KeyVerificationRequest>()?.asValidObject() + + if (validRequestInfo == null) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + val senderId = event.senderId ?: return + + // We don't want to block here + val otherDeviceId = validRequestInfo.fromDevice + + cryptoCoroutineScope.launch { + if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { + Timber.e("## Verification device $otherDeviceId is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = null, + transactionId = validRequestInfo.transactionId, + localId = validRequestInfo.transactionId, + requestInfo = validRequestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + } + + suspend fun onRoomRequestReceived(event: Event) { + Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>() ?: return + val validRequestInfo = requestInfo + // copy the EventId to the transactionId + .copy(transactionId = event.eventId) + .asValidObject() ?: return + + val senderId = event.senderId ?: return + + if (requestInfo.toUserId != userId) { + // I should ignore this, it's not for me + Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") + return + } + + // We don't want to block here + taskExecutor.executorScope.launch { + if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) { + Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = event.roomId, + transactionId = event.eventId, + localId = event.eventId!!, + requestInfo = validRequestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ + } + + override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // When Should/Can we cancel?? + val relationContent = event.content.toModel<EncryptedEventContent>()?.relatesTo + if (relationContent?.type == RelationType.REFERENCE) { + val relatedId = relationContent.eventId ?: return + // at least if request was sent by me, I can safely cancel without interfering + pendingRequests[event.senderId]?.firstOrNull { + it.transactionId == relatedId && !it.isIncoming + }?.let { pr -> + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + relatedId, + event.senderId ?: "", + event.getSenderKey() ?: "", + CancelCode.InvalidMessage + ) + updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) + } + } + } + + private suspend fun onRoomStartRequestReceived(event: Event) { + val startReq = event.getClearContent().toModel<MessageVerificationStartContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + + val validStartReq = startReq?.asValidObject() + + val otherUserId = event.senderId + if (validStartReq == null) { + Timber.e("## received invalid verification request") + if (startReq?.transactionId != null) { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + startReq.transactionId ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + + handleStart(otherUserId, validStartReq) { + it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) + }?.let { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + validStartReq.transactionId, + otherUserId!!, + validStartReq.fromDevice, + it + ) + } + } + + private suspend fun onStartRequestReceived(event: Event) { + Timber.e("## SAS received Start request ${event.eventId}") + val startReq = event.getClearContent().toModel<KeyVerificationStart>() + val validStartReq = startReq?.asValidObject() + Timber.v("## SAS received Start request $startReq") + + val otherUserId = event.senderId!! + if (validStartReq == null) { + Timber.e("## SAS received invalid verification request") + if (startReq?.transactionId != null) { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + startReq.transactionId, + otherUserId, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + // Download device keys prior to everything + handleStart(otherUserId, validStartReq) { + it.transport = verificationTransportToDeviceFactory.createTransport(it) + }?.let { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + validStartReq.transactionId, + otherUserId, + validStartReq.fromDevice, + it + ) + } + } + + /** + * Return a CancelCode to make the caller cancel the verification. Else return null + */ + private suspend fun handleStart(otherUserId: String?, + startReq: ValidVerificationInfoStart, + txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { + Timber.d("## SAS onStartRequestReceived $startReq") + if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { + val tid = startReq.transactionId + var existing = getExistingTransaction(otherUserId, tid) + + // After the m.key.verification.ready event is sent, either party can send an + // m.key.verification.start event to begin the verification. If both parties + // send an m.key.verification.start event, and they both specify the same + // verification method, then the event sent by the user whose user ID is the + // smallest is used, and the other m.key.verification.start event is ignored. + // In the case of a single user verifying two of their devices, the device ID is + // compared instead . + if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { + val readyRequest = getExistingVerificationRequest(otherUserId, tid) + if (readyRequest?.isReady == true) { + if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { + Timber.d("## SAS concurrent start isOtherPrioritary, clear") + // The other is prioritary! + // I should replace my outgoing with an incoming + removeTransaction(otherUserId, tid) + existing = null + } else { + Timber.d("## SAS concurrent start i am prioritary, ignore") + // i am prioritary, ignore this start event! + return null + } + } + } + + when (startReq) { + is ValidVerificationInfoStart.SasVerificationInfoStart -> { + when (existing) { + is SasVerificationTransaction -> { + // should cancel both! + Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionId}") + existing.cancel(CancelCode.UnexpectedMessage) + // Already cancelled, so return null + return null + } + is QrCodeVerificationTransaction -> { + // Nothing to do? + } + null -> { + getExistingTransactionsForUser(otherUserId) + ?.filterIsInstance(SasVerificationTransaction::class.java) + ?.takeIf { it.isNotEmpty() } + ?.also { + // Multiple keyshares between two devices: + // any two devices may only have at most one key verification in flight at a time. + Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionId}") + } + ?.forEach { + it.cancel(CancelCode.UnexpectedMessage) + } + ?.also { + return CancelCode.UnexpectedMessage + } + } + } + + // Ok we can create a SAS transaction + Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionId}") + // If there is a corresponding request, we can auto accept + // as we are the one requesting in first place (or we accepted the request) + // I need to check if the pending request was related to this device also + val autoAccept = getExistingVerificationRequest(otherUserId)?.any { + it.transactionId == startReq.transactionId + && (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) + } + ?: false + val tx = DefaultIncomingSASDefaultVerificationTransaction( +// this, + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + startReq.transactionId, + otherUserId, + autoAccept).also { txConfigure(it) } + addTransaction(tx) + tx.onVerificationStart(startReq) + return null + } + is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> { + // Other user has scanned my QR code + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onStartReceived(startReq) + return null + } else { + Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") + return CancelCode.UnexpectedMessage + } + } + } + } else { + return CancelCode.UnexpectedMessage + } + } + + private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean { + if (userId < otherUserId) { + return false + } else if (userId > otherUserId) { + return true + } else { + return otherDeviceId < deviceId ?: "" + } + } + + // TODO Refacto: It could just return a boolean + private suspend fun checkKeysAreDownloaded(otherUserId: String, + otherDeviceId: String): MXUsersDevicesMap<CryptoDeviceInfo>? { + return try { + var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) + if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) { + return keys + } else { + // force download + keys = deviceListManager.downloadKeys(listOf(otherUserId), true) + return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true } + } + } catch (e: Exception) { + null + } + } + + private fun onRoomCancelReceived(event: Event) { + val cancelReq = event.getClearContent().toModel<MessageVerificationCancelContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + + val validCancelReq = cancelReq?.asValidObject() + + if (validCancelReq == null) { + // ignore + Timber.e("## SAS Received invalid cancel request") + // TODO should we cancel? + return + } + getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let { + updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code))) + // Should we remove it from the list? + } + handleOnCancel(event.senderId!!, validCancelReq) + } + + private fun onCancelReceived(event: Event) { + Timber.v("## SAS onCancelReceived") + val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()?.asValidObject() + + if (cancelReq == null) { + // ignore + Timber.e("## SAS Received invalid cancel request") + return + } + val otherUserId = event.senderId!! + + handleOnCancel(otherUserId, cancelReq) + } + + private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) { + Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}") + + val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId) + val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId) + + if (existingRequest != null) { + // Mark this request as cancelled + updatePendingRequest(existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + )) + } + + existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) + } + + private fun onRoomAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept via DM $event") + val accept = event.getClearContent().toModel<MessageVerificationAcceptContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + ?: return + + val validAccept = accept.asValidObject() ?: return + + handleAccept(validAccept, event.senderId!!) + } + + private fun onAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept $event") + val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>()?.asValidObject() ?: return + handleAccept(acceptReq, event.senderId!!) + } + + private fun handleAccept(acceptReq: ValidVerificationInfoAccept, senderId: String) { + val otherUserId = senderId + val existing = getExistingTransaction(otherUserId, acceptReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid accept request") + return + } + + if (existing is SASDefaultVerificationTransaction) { + existing.onVerificationAccept(acceptReq) + } else { + // not other types now + } + } + + private fun onRoomKeyRequestReceived(event: Event) { + val keyReq = event.getClearContent().toModel<MessageVerificationKeyContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + ?.asValidObject() + if (keyReq == null) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + handleKeyReceived(event, keyReq) + } + + private fun onKeyReceived(event: Event) { + val keyReq = event.getClearContent().toModel<KeyVerificationKey>()?.asValidObject() + + if (keyReq == null) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + handleKeyReceived(event, keyReq) + } + + private fun handleKeyReceived(event: Event, keyReq: ValidVerificationInfoKey) { + Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") + val otherUserId = event.senderId!! + val existing = getExistingTransaction(otherUserId, keyReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid key request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.onKeyVerificationKey(keyReq) + } else { + // not other types now + } + } + + private fun onRoomMacReceived(event: Event) { + val macReq = event.getClearContent().toModel<MessageVerificationMacContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + ?.asValidObject() + if (macReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + // TODO should we cancel? + return + } + handleMacReceived(event.senderId, macReq) + } + + private suspend fun onRoomReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + ?.asValidObject() + if (readyReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + } + } + + private suspend fun onReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject() + Timber.v("## SAS onReadyReceived $readyReq") + + if (readyReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportToDeviceFactory.createTransport(it) + } + } + + private fun onDoneReceived(event: Event) { + Timber.v("## onDoneReceived") + val doneReq = event.getClearContent().toModel<KeyVerificationDone>()?.asValidObject() + if (doneReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid done request") + return + } + + handleDoneReceived(event.senderId, doneReq) + + if (event.senderId == userId) { + // We only send gossiping request when the other sent us a done + // We can ask without checking too much thinks (like trust), because we will check validity of secret on reception + getExistingTransaction(userId, doneReq.transactionId) + ?: getOldTransaction(userId, doneReq.transactionId) + ?.let { vt -> + val otherDeviceId = vt.otherDeviceId + if (!crossSigningService.canCrossSign()) { + outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + } + } + + private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) { + Timber.v("## SAS Done received $doneReq") + val existing = getExistingTransaction(senderId, doneReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid Done request") + return + } + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onDoneReceived() + } else { + // SAS do not care for now? + } + + // Now transactions are udated, let's also update Requests + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneReq.transactionId } + if (existingRequest == null) { + Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}") + return + } + updatePendingRequest(existingRequest.copy(isSuccessful = true)) + } + + private fun onRoomDoneReceived(event: Event) { + val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo + ) + ?.asValidObject() + + if (doneReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid Done request") + // TODO should we cancel? + return + } + + handleDoneReceived(event.senderId, doneReq) + } + + private fun onMacReceived(event: Event) { + val macReq = event.getClearContent().toModel<KeyVerificationMac>()?.asValidObject() + + if (macReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + return + } + handleMacReceived(event.senderId, macReq) + } + + private fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) { + Timber.v("## SAS Received $macReq") + val existing = getExistingTransaction(senderId, macReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid Mac request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.onKeyVerificationMac(macReq) + } else { + // not other types known for now + } + } + + private fun handleReadyReceived(senderId: String, + readyReq: ValidVerificationInfoReady, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionId } + if (existingRequest == null) { + Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionId} fromDevice ${readyReq.fromDevice}") + return + } + + val qrCodeData = readyReq.methods + // Check if other user is able to scan QR code + .takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } + ?.let { + createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice) + } + + if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = readyReq.transactionId, + otherUserId = senderId, + otherDeviceId = readyReq.fromDevice, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) + + tx.transport = transportCreator.invoke(tx) + + addTransaction(tx) + } + + updatePendingRequest(existingRequest.copy( + readyInfo = readyReq + )) + } + + private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { + requestId ?: run { + Timber.w("## Unknown requestId") + return null + } + + return when { + userId != otherUserId -> + createQrCodeDataForDistinctUser(requestId, otherUserId) + crossSigningService.isCrossSigningVerified() -> + // This is a self verification and I am the old device (Osborne2) + createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) + else -> + // This is a self verification and I am the new device (Dynabook) + createQrCodeDataForUnVerifiedDevice(requestId) + } + } + + private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId) + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get other user master key") + return null + } + + return QrCodeData.VerifyingAnotherUser( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherUserMasterCrossSigningPublicKey = otherUserMasterKey, + sharedSecret = generateSharedSecretV2() + ) + } + + // Create a QR code to display on the old device (Osborne2) + private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherDeviceKey = otherDeviceId + ?.let { + cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() + } + ?: run { + Timber.w("## Unable to get other device data") + return null + } + + return QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherDeviceKey = otherDeviceKey, + sharedSecret = generateSharedSecretV2() + ) + } + + // Create a QR code to display on the new device (Dynabook) + private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() + ?: run { + Timber.w("## Unable to get my fingerprint") + return null + } + + return QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = requestId, + deviceKey = myDeviceKey, + userMasterCrossSigningPublicKey = myMasterKey, + sharedSecret = generateSharedSecretV2() + ) + } + +// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { +// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } +// if (existingRequest == null) { +// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") +// return +// } +// updatePendingRequest(existingRequest.copy(isSuccessful = true)) +// } + + // TODO All this methods should be delegated to a TransactionStore + override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { + synchronized(lock = txMap) { + return txMap[otherUserId]?.get(tid) + } + } + + override fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>? { + synchronized(lock = pendingRequests) { + return pendingRequests[otherUserId] + } + } + + override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } } + } + } + + override fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> + pendingRequests.flatMap { entry -> + entry.value.filter { it.roomId == roomId && it.transactionId == tid } + }.firstOrNull() + } + } + } + + private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? { + synchronized(txMap) { + return txMap[otherUser]?.values + } + } + + private fun removeTransaction(otherUser: String, tid: String) { + synchronized(txMap) { + txMap[otherUser]?.remove(tid)?.also { + it.removeListener(this) + } + }?.let { + rememberOldTransaction(it) + } + } + + private fun addTransaction(tx: DefaultVerificationTransaction) { + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) + } + } + + private fun rememberOldTransaction(tx: DefaultVerificationTransaction) { + synchronized(pastTransactions) { + pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx + } + } + + private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? { + return tid?.let { + synchronized(pastTransactions) { + pastTransactions[userId]?.get(it) + } + } + } + + override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { + val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) + // should check if already one (and cancel it) + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + txID, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) + addTransaction(tx) + + tx.start() + return txID + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, otherUserId: String, roomId: String, localId: String?) + : PendingVerificationRequest { + Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") + + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } + + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + + // Cancel existing pending requests? + requestsForUser.toList().forEach { existingRequest -> + existingRequest.transactionId?.let { tid -> + if (!existingRequest.isFinished) { + Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) + transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) + } + } + } + + val validLocalId = localId ?: LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = roomId, + localId = validLocalId, + otherUserId = otherUserId + ) + + // We can SCAN or SHOW QR codes only if cross-signing is verified + val methodValues = if (crossSigningService.isCrossSigningVerified()) { + // Add reciprocate method if application declares it can scan or show QR codes + // Not sure if it ok to do that (?) + val reciprocateMethod = methods + .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } + ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() + methods.map { it.toValue() } + reciprocateMethod + } else { + // Filter out SCAN and SHOW qr code method + methods + .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } + .map { it.toValue() } + } + .distinct() + + transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> + // We need to update with the syncedID + updatePendingRequest(verificationRequest.copy( + transactionId = syncedId, + // localId stays different + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun cancelVerificationRequest(request: PendingVerificationRequest) { + if (request.roomId != null) { + val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null) + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User) + } else { + val transport = verificationTransportToDeviceFactory.createTransport(null) + request.targetDevices?.forEach { deviceId -> + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User) + } + } + } + + override fun requestKeyVerification(methods: List<VerificationMethod>, otherUserId: String, otherDevices: List<String>?): PendingVerificationRequest { + // TODO refactor this with the DM one + Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") + + val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } + + val transport = verificationTransportToDeviceFactory.createTransport(null) + + // Cancel existing pending requests? + requestsForUser.toList().forEach { existingRequest -> + existingRequest.transactionId?.let { tid -> + if (!existingRequest.isFinished) { + Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) + existingRequest.targetDevices?.forEach { + transport.cancelTransaction(tid, existingRequest.otherUserId, it, CancelCode.User) + } + } + } + } + + val localId = LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + transactionId = localId, + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = null, + localId = localId, + otherUserId = otherUserId, + targetDevices = targetDevices + ) + + // We can SCAN or SHOW QR codes only if cross-signing is enabled + val methodValues = if (crossSigningService.isCrossSigningInitialized()) { + // Add reciprocate method if application declares it can scan or show QR codes + // Not sure if it ok to do that (?) + val reciprocateMethod = methods + .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } + ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() + methods.map { it.toValue() } + reciprocateMethod + } else { + // Filter out SCAN and SHOW qr code method + methods + .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } + .map { it.toValue() } + } + .distinct() + + transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info -> + // Nothing special to do in to device mode + updatePendingRequest(verificationRequest.copy( + // localId stays different + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { + verificationTransportRoomMessageFactory.createTransport(roomId, null) + .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) + + getExistingVerificationRequest(otherUserId, transactionId)?.let { + updatePendingRequest(it.copy( + cancelConclusion = CancelCode.User + )) + } + } + + private fun updatePendingRequest(updated: PendingVerificationRequest) { + val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() } + val index = requestsForUser.indexOfFirst { + it.transactionId == updated.transactionId + || it.transactionId == null && it.localId == updated.localId + } + if (index != -1) { + requestsForUser.removeAt(index) + } + requestsForUser.add(updated) + dispatchRequestUpdated(updated) + } + + override fun beginKeyVerificationInDMs(method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback<String>?): String? { + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + addTransaction(tx) + + tx.start() + return transactionId + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun readyPendingVerificationInDMs(methods: List<VerificationMethod>, + otherUserId: String, + roomId: String, + transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") + // Let's find the related request + val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) + if (existingRequest != null) { + // we need to send a ready event, with matching methods + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportRoomMessageFactory.createTransport(roomId, it) + } + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + // TODO buttons should not be shown in this case? + return false + } + // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, + readyMsg, + VerificationTxState.None, + CancelCode.User, + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) + return true + } else { + Timber.e("## SAS readyPendingVerificationInDMs Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + override fun readyPendingVerification(methods: List<VerificationMethod>, + otherUserId: String, + transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerification $otherUserId tx:$transactionId") + // Let's find the related request + val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) + if (existingRequest != null) { + // we need to send a ready event, with matching methods + val transport = verificationTransportToDeviceFactory.createTransport(null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportToDeviceFactory.createTransport(it) + } + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + // TODO buttons should not be shown in this case? + return false + } + // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) + transport.sendVerificationReady( + readyMsg, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) + return true + } else { + Timber.e("## SAS readyPendingVerification Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + private fun computeReadyMethods( + transactionId: String, + otherUserId: String, + otherDeviceId: String, + otherUserMethods: List<String>?, + methods: List<VerificationMethod>, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport): List<String> { + if (otherUserMethods.isNullOrEmpty()) { + return emptyList() + } + + val result = mutableSetOf<String>() + + if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { + // Other can do SAS and so do I + result.add(VERIFICATION_METHOD_SAS) + } + + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { + // Other user wants to verify using QR code. Cross-signing has to be setup + val qrCodeData = createQrCodeData(transactionId, otherUserId, otherDeviceId) + + if (qrCodeData != null) { + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { + // Other can Scan and I can show QR code + result.add(VERIFICATION_METHOD_QR_CODE_SHOW) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { + // Other can show and I can scan QR code + result.add(VERIFICATION_METHOD_QR_CODE_SCAN) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + } + + if (VERIFICATION_METHOD_RECIPROCATE in result) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = transactionId, + otherUserId = otherUserId, + otherDeviceId = otherDeviceId, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) + + tx.transport = transportCreator.invoke(tx) + + addTransaction(tx) + } + } + + return result.toList() + } + + /** + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid + */ + private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String { + return buildString { + append(userId).append("|") + append(deviceId).append("|") + append(otherUserId).append("|") + append(otherDeviceID).append("|") + append(UUID.randomUUID().toString()) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + dispatchTxUpdated(tx) + if (tx.state is VerificationTxState.TerminalTxState) { + // remove + this.removeTransaction(tx.otherUserId, tx.transactionId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4f559767c7d609b39c12e64a80ed227a12036bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +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.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import timber.log.Timber + +/** + * Generic interactive key verification transaction + */ +internal abstract class DefaultVerificationTransaction( + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val crossSigningService: CrossSigningService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val userId: String, + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String? = null, + override val isIncoming: Boolean) : VerificationTransaction { + + lateinit var transport: VerificationTransport + + interface Listener { + fun transactionUpdated(tx: VerificationTransaction) + } + + protected var listeners = ArrayList<Listener>() + + fun addListener(listener: Listener) { + if (!listeners.contains(listener)) listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + protected fun trust(canTrustOtherUserMasterKey: Boolean, + toVerifyDeviceIds: List<String>, + eventuallyMarkMyMasterKeyAsTrusted: Boolean, autoDone : Boolean = true) { + Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds") + Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted") + + // TODO what if the otherDevice is not in this list? and should we + toVerifyDeviceIds.forEach { + setDeviceVerified(otherUserId, it) + } + + // If not me sign his MSK and upload the signature + if (canTrustOtherUserMasterKey) { + // we should trust this master key + // And check verification MSK -> SSK? + if (otherUserId != userId) { + crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## Verification: Failed to trust User $otherUserId") + } + }) + } else { + // Notice other master key is mine because other is me + if (eventuallyMarkMyMasterKeyAsTrusted) { + // Mark my keys as trusted locally + crossSigningService.markMyMasterKeyAsTrusted() + } + } + } + + if (otherUserId == userId) { + incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + + // If me it's reasonable to sign and upload the device signature + // Notice that i might not have the private keys, so may not be able to do it + crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> { + override fun onFailure(failure: Throwable) { + Timber.w("## Verification: Failed to sign new device $otherDeviceId, ${failure.localizedMessage}") + } + }) + } + + if (autoDone) { + state = VerificationTxState.Verified + transport.done(transactionId) {} + } + } + + private fun setDeviceVerified(userId: String, deviceId: String) { + // TODO should not override cross sign status + setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + userId, + deviceId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ebd3b51b0af40d12aabf0fc1d44347c8ea13dd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -0,0 +1,421 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.extensions.toUnsignedInt +import org.matrix.android.sdk.internal.util.withoutPrefix +import org.matrix.olm.OlmSAS +import org.matrix.olm.OlmUtility +import timber.log.Timber + +/** + * Represents an ongoing short code interactive key verification between two devices. + */ +internal abstract class SASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + open val userId: String, + open val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String?, + isIncoming: Boolean +) : DefaultVerificationTransaction( + setDeviceVerificationAction, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + userId, + transactionId, + otherUserId, + otherDeviceId, + isIncoming), + SasVerificationTransaction { + + companion object { + const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" + const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" + + // Deprecated maybe removed later, use V2 + const val KEY_AGREEMENT_V1 = "curve25519" + const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256" + // ordered by preferred order + val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1) + // ordered by preferred order + val KNOWN_HASHES = listOf("sha256") + // ordered by preferred order + val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) + + // older devices have limited support of emoji but SDK offers images for the 64 verification emojis + // so always send that we support EMOJI + val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL) + } + + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + + if (newState is VerificationTxState.TerminalTxState) { + releaseSAS() + } + } + + private var olmSas: OlmSAS? = null + + // Visible for test + var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null + // Visible for test + var accepted: ValidVerificationInfoAccept? = null + protected var otherKey: String? = null + protected var shortCodeBytes: ByteArray? = null + + protected var myMac: ValidVerificationInfoMac? = null + protected var theirMac: ValidVerificationInfoMac? = null + + protected fun getSAS(): OlmSAS { + if (olmSas == null) olmSas = OlmSAS() + return olmSas!! + } + + // To override finalize(), all you need to do is simply declare it, without using the override keyword: + protected fun finalize() { + releaseSAS() + } + + private fun releaseSAS() { + // finalization logic + olmSas?.releaseSas() + olmSas = null + } + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + override fun userHasVerifiedShortCode() { + Timber.v("## SAS short code verified by user for id:$transactionId") + if (state != VerificationTxState.ShortCodeReady) { + // ignore and cancel? + Timber.e("## Accepted short code from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + state = VerificationTxState.ShortCodeAccepted + // Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated, + // sorted list of the key IDs that they wish the other user to verify, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_MACâ€, + // - the Matrix ID of the user whose key is being MAC-ed, + // - the device ID of the device sending the MAC, + // - the Matrix ID of the other user, + // - the device ID of the device receiving the MAC, + // - the transaction ID, and + // - the key ID of the key being MAC-ed, or the string “KEY_IDS†if the item being MAC-ed is the list of key IDs. + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId" + + // Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key. + // It should now contain both the device key and the MSK. + // So when Alice and Bob verify with SAS, the verification will verify the MSK. + + val keyMap = HashMap<String, String>() + + val keyId = "ed25519:$deviceId" + val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId) + + if (macString.isNullOrBlank()) { + // Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + keyMap[keyId] = macString + + cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() } + ?.masterKey() + ?.unpaddedBase64PublicKey + ?.let { masterPublicKey -> + val crossSigningKeyId = "ed25519:$masterPublicKey" + macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString -> + keyMap[crossSigningKeyId] = MSKMacString + } + } + + val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS") + + if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) { + // Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + val macMsg = transport.createMac(transactionId, keyMap, keyStrings) + myMac = macMsg.asValidObject() + state = VerificationTxState.SendingMac + sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) { + if (state == VerificationTxState.SendingMac) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.MacSent + } + } + + // Do I already have their Mac? + theirMac?.let { verifyMacs(it) } + // if not wait for it + } + + override fun shortCodeDoesNotMatch() { + Timber.v("## SAS short code do not match for id:$transactionId") + cancel(CancelCode.MismatchedSas) + } + + override fun isToDeviceTransport(): Boolean { + return transport is VerificationTransportToDevice + } + + abstract fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) + + abstract fun onVerificationAccept(accept: ValidVerificationInfoAccept) + + abstract fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) + + abstract fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) + + protected fun verifyMacs(theirMacSafe: ValidVerificationInfoMac) { + Timber.v("## SAS verifying macs for id:$transactionId") + state = VerificationTxState.Verifying + + // Keys have been downloaded earlier in process + val otherUserKnownDevices = cryptoStore.getUserDevices(otherUserId) + + // Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID), + // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. + // Bob’s device compares these with the HMAC values given in the m.key.verification.mac message. + // If everything matches, then consider Alice’s device keys as verified. + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$otherUserId$otherDeviceId$userId$deviceId$transactionId" + + val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",") + + val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") + if (theirMacSafe.keys != keyStrings) { + // WRONG! + cancel(CancelCode.MismatchedKeys) + return + } + + val verifiedDevices = ArrayList<String>() + + // cannot be empty because it has been validated + theirMacSafe.mac.keys.forEach { + val keyIDNoPrefix = it.withoutPrefix("ed25519:") + val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() + if (otherDeviceKey == null) { + Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") + // just ignore and continue + return@forEach + } + val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) + if (mac != theirMacSafe.mac[it]) { + // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") + cancel(CancelCode.MismatchedKeys) + return + } + verifiedDevices.add(keyIDNoPrefix) + } + + var otherMasterKeyIsVerified = false + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey + if (otherCrossSigningMasterKeyPublic != null) { + // Did the user signed his master key + theirMacSafe.mac.keys.forEach { + val keyIDNoPrefix = it.withoutPrefix("ed25519:") + if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { + // Check the signature + val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) + if (mac != theirMacSafe.mac[it]) { + // WRONG! + Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") + cancel(CancelCode.MismatchedKeys) + return + } else { + otherMasterKeyIsVerified = true + } + } + } + } + + // if none of the keys could be verified, then error because the app + // should be informed about that + if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) { + Timber.e("## SAS Verification: No devices verified") + cancel(CancelCode.MismatchedKeys) + return + } + + trust(otherMasterKeyIsVerified, + verifiedDevices, + eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false) + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + state = VerificationTxState.Cancelled(code, true) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) + } + + protected fun <T> sendToOther(type: String, + keyToDevice: VerificationInfo<T>, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) + } + + fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { + if (shortCodeBytes == null) { + return null + } + when (shortAuthenticationStringMode) { + SasMode.DECIMAL -> { + if (shortCodeBytes!!.size < 5) return null + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + SasMode.EMOJI -> { + if (shortCodeBytes!!.size < 6) return null + return getEmojiCodeRepresentation(shortCodeBytes!!).joinToString(" ") { it.emoji } + } + else -> return null + } + } + + override fun supportsEmoji(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI).orFalse() + } + + override fun supportsDecimal(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL).orFalse() + } + + protected fun hashUsingAgreedHashMethod(toHash: String): String? { + if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + val olmUtil = OlmUtility() + val hashBytes = olmUtil.sha256(toHash) + olmUtil.releaseUtility() + return hashBytes + } + return null + } + + private fun macUsingAgreedMethod(message: String, info: String): String? { + if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMacLongKdf(message, info) + } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMac(message, info) + } + return null + } + + override fun getDecimalCodeRepresentation(): String { + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + + /** + * decimal: generate five bytes by using HKDF. + * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive), + * and add 1000 (resulting in a number between 1000 and 9191 inclusive). + * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers. + * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000, + * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000. + * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic, + * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.) + * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers, + * or with the three numbers on separate lines. + */ + fun getDecimalCodeRepresentation(byteArray: ByteArray): String { + val b0 = byteArray[0].toUnsignedInt() // need unsigned byte + val b1 = byteArray[1].toUnsignedInt() // need unsigned byte + val b2 = byteArray[2].toUnsignedInt() // need unsigned byte + val b3 = byteArray[3].toUnsignedInt() // need unsigned byte + val b4 = byteArray[4].toUnsignedInt() // need unsigned byte + // (B0 << 5 | B1 >> 3) + 1000 + val first = (b0.shl(5) or b1.shr(3)) + 1000 + // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 + val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000 + // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000 + val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000 + return "$first $second $third" + } + + override fun getEmojiCodeRepresentation(): List<EmojiRepresentation> { + return getEmojiCodeRepresentation(shortCodeBytes!!) + } + + /** + * emoji: generate six bytes by using HKDF. + * Split the first 42 bits into 7 groups of 6 bits, as one would do when creating a base64 encoding. + * For each group of 6 bits, look up the emoji from Appendix A corresponding + * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) + */ + private fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> { + val b0 = byteArray[0].toUnsignedInt() + val b1 = byteArray[1].toUnsignedInt() + val b2 = byteArray[2].toUnsignedInt() + val b3 = byteArray[3].toUnsignedInt() + val b4 = byteArray[4].toUnsignedInt() + val b5 = byteArray[5].toUnsignedInt() + return listOf( + getEmojiForCode((b0 and 0xFC).shr(2)), + getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), + getEmojiForCode((b1 and 0xF).shl(2) or (b2 and 0xC0).shr(6)), + getEmojiForCode((b2 and 0x3F)), + getEmojiForCode((b3 and 0xFC).shr(2)), + getEmojiForCode((b3 and 0x3).shl(4) or (b4 and 0xF0).shr(4)), + getEmojiForCode((b4 and 0xF).shl(2) or (b5 and 0xC0).shr(6)) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b7d26e76bc37493060bef62563d350f131136bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class SendVerificationMessageWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val event: Event, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject + lateinit var sendVerificationMessageTask: SendVerificationMessageTask + + @Inject + lateinit var cryptoService: CryptoService + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + val localId = params.event.eventId ?: "" + return try { + val eventId = sendVerificationMessageTask.execute( + SendVerificationMessageTask.Params( + event = params.event, + cryptoService = cryptoService + ) + ) + + Result.success(Data.Builder().putString(localId, eventId).build()) + } catch (exception: Throwable) { + if (exception.shouldBeRetried()) { + Result.retry() + } else { + Result.success(errorOutputData) + } + } + } + + companion object { + private const val OUTPUT_KEY_FAILED = "failed" + + fun hasFailed(outputData: Data): Boolean { + return outputData.getBoolean(OUTPUT_KEY_FAILED, false) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a55ec2a9c7214d908125cdecf8efba81038ae45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation + +internal fun getEmojiForCode(code: Int): EmojiRepresentation { + return when (code % 64) { + 0 -> EmojiRepresentation("ðŸ¶", R.string.verification_emoji_dog, R.drawable.ic_verification_dog) + 1 -> EmojiRepresentation("ðŸ±", R.string.verification_emoji_cat, R.drawable.ic_verification_cat) + 2 -> EmojiRepresentation("ðŸ¦", R.string.verification_emoji_lion, R.drawable.ic_verification_lion) + 3 -> EmojiRepresentation("ðŸŽ", R.string.verification_emoji_horse, R.drawable.ic_verification_horse) + 4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn, R.drawable.ic_verification_unicorn) + 5 -> EmojiRepresentation("ðŸ·", R.string.verification_emoji_pig, R.drawable.ic_verification_pig) + 6 -> EmojiRepresentation("ðŸ˜", R.string.verification_emoji_elephant, R.drawable.ic_verification_elephant) + 7 -> EmojiRepresentation("ðŸ°", R.string.verification_emoji_rabbit, R.drawable.ic_verification_rabbit) + 8 -> EmojiRepresentation("ðŸ¼", R.string.verification_emoji_panda, R.drawable.ic_verification_panda) + 9 -> EmojiRepresentation("ðŸ“", R.string.verification_emoji_rooster, R.drawable.ic_verification_rooster) + 10 -> EmojiRepresentation("ðŸ§", R.string.verification_emoji_penguin, R.drawable.ic_verification_penguin) + 11 -> EmojiRepresentation("ðŸ¢", R.string.verification_emoji_turtle, R.drawable.ic_verification_turtle) + 12 -> EmojiRepresentation("ðŸŸ", R.string.verification_emoji_fish, R.drawable.ic_verification_fish) + 13 -> EmojiRepresentation("ðŸ™", R.string.verification_emoji_octopus, R.drawable.ic_verification_octopus) + 14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly, R.drawable.ic_verification_butterfly) + 15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower, R.drawable.ic_verification_flower) + 16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree, R.drawable.ic_verification_tree) + 17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus, R.drawable.ic_verification_cactus) + 18 -> EmojiRepresentation("ðŸ„", R.string.verification_emoji_mushroom, R.drawable.ic_verification_mushroom) + 19 -> EmojiRepresentation("ðŸŒ", R.string.verification_emoji_globe, R.drawable.ic_verification_globe) + 20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon, R.drawable.ic_verification_moon) + 21 -> EmojiRepresentation("â˜ï¸", R.string.verification_emoji_cloud, R.drawable.ic_verification_cloud) + 22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire, R.drawable.ic_verification_fire) + 23 -> EmojiRepresentation("ðŸŒ", R.string.verification_emoji_banana, R.drawable.ic_verification_banana) + 24 -> EmojiRepresentation("ðŸŽ", R.string.verification_emoji_apple, R.drawable.ic_verification_apple) + 25 -> EmojiRepresentation("ðŸ“", R.string.verification_emoji_strawberry, R.drawable.ic_verification_strawberry) + 26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn, R.drawable.ic_verification_corn) + 27 -> EmojiRepresentation("ðŸ•", R.string.verification_emoji_pizza, R.drawable.ic_verification_pizza) + 28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake, R.drawable.ic_verification_cake) + 29 -> EmojiRepresentation("â¤ï¸", R.string.verification_emoji_heart, R.drawable.ic_verification_heart) + 30 -> EmojiRepresentation("🙂", R.string.verification_emoji_smiley, R.drawable.ic_verification_smiley) + 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot) + 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat) + 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses) + 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench) + 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa) + 36 -> EmojiRepresentation("ðŸ‘", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up) + 37 -> EmojiRepresentation("☂ï¸", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella) + 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass) + 39 -> EmojiRepresentation("â°", R.string.verification_emoji_clock, R.drawable.ic_verification_clock) + 40 -> EmojiRepresentation("ðŸŽ", R.string.verification_emoji_gift, R.drawable.ic_verification_gift) + 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb) + 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book) + 43 -> EmojiRepresentation("âœï¸", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil) + 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip) + 45 -> EmojiRepresentation("✂ï¸", R.string.verification_emoji_scissors, R.drawable.ic_verification_scissors) + 46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock, R.drawable.ic_verification_lock) + 47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key, R.drawable.ic_verification_key) + 48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer, R.drawable.ic_verification_hammer) + 49 -> EmojiRepresentation("☎ï¸", R.string.verification_emoji_telephone, R.drawable.ic_verification_phone) + 50 -> EmojiRepresentation("ðŸ", R.string.verification_emoji_flag, R.drawable.ic_verification_flag) + 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train) + 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle) + 53 -> EmojiRepresentation("✈ï¸", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane) + 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket) + 55 -> EmojiRepresentation("ðŸ†", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy) + 56 -> EmojiRepresentation("âš½", R.string.verification_emoji_ball, R.drawable.ic_verification_ball) + 57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar, R.drawable.ic_verification_guitar) + 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet) + 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell) + 60 -> EmojiRepresentation("âš“", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor) + 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone) + 62 -> EmojiRepresentation("ðŸ“", R.string.verification_emoji_folder, R.drawable.ic_verification_folder) + /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f4c4e9c9382ab38ee9e5361e8a34f6b4b26e53f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceObject + +interface VerificationInfo<ValidObjectType> { + fun toEventContent(): Content? = null + fun toSendToDeviceObject(): SendToDeviceObject? = null + + fun asValidObject(): ValidObjectType? + + /** + * String to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + val transactionId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c6435c1cdce0455e1fc350ba043709af5f9fd08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoAccept : VerificationInfo<ValidVerificationInfoAccept> { + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val keyAgreementProtocol: String? + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val hash: String? + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val messageAuthenticationCode: String? + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + val shortAuthenticationStrings: List<String>? + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + var commitment: String? + + override fun asValidObject(): ValidVerificationInfoAccept? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null + val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null + val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null + val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null + val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoAccept( + validTransactionId, + validKeyAgreementProtocol, + validHash, + validMessageAuthenticationCode, + validShortAuthenticationStrings, + validCommitment + ) + } +} + +internal interface VerificationInfoAcceptFactory { + + fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>): VerificationInfoAccept +} + +internal data class ValidVerificationInfoAccept( + val transactionId: String, + val keyAgreementProtocol: String, + val hash: String, + val messageAuthenticationCode: String, + val shortAuthenticationStrings: List<String>, + var commitment: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt new file mode 100644 index 0000000000000000000000000000000000000000..68282cb92525c4ef3dd607dbdb75d84ebcc521b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoCancel : VerificationInfo<ValidVerificationInfoCancel> { + /** + * machine-readable reason for cancelling, see [CancelCode] + */ + val code: String? + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + val reason: String? + + override fun asValidObject(): ValidVerificationInfoCancel? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validCode = code?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoCancel( + validTransactionId, + validCode, + reason + ) + } +} + +internal data class ValidVerificationInfoCancel( + val transactionId: String, + val code: String, + val reason: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt new file mode 100644 index 0000000000000000000000000000000000000000..7dce847e3034b3bf1ffc8fe56a7ff93cf00d3d07 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone + +internal interface VerificationInfoDone : VerificationInfo<ValidVerificationDone> { + + override fun asValidObject(): ValidVerificationDone? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + return ValidVerificationDone(validTransactionId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..745309df796cbc1b10c289e142ee67b8089fa859 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +internal interface VerificationInfoKey : VerificationInfo<ValidVerificationInfoKey> { + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + val key: String? + + override fun asValidObject(): ValidVerificationInfoKey? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validKey = key?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoKey( + validTransactionId, + validKey + ) + } +} + +internal interface VerificationInfoKeyFactory { + fun create(tid: String, pubKey: String): VerificationInfoKey +} + +internal data class ValidVerificationInfoKey( + val transactionId: String, + val key: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ffd0556f586361cec42113c73504475e301b145 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoMac : VerificationInfo<ValidVerificationInfoMac> { + /** + * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key + */ + val mac: Map<String, String>? + + /** + * The MAC of the comma-separated, sorted list of key IDs given in the mac property, + * as an unpadded base64 string, calculated using the MAC key. + * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will + * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMNâ€. + */ + val keys: String? + + override fun asValidObject(): ValidVerificationInfoMac? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null + val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoMac( + validTransactionId, + validMac, + validKeys + ) + } +} + +internal interface VerificationInfoMacFactory { + fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac +} + +internal data class ValidVerificationInfoMac( + val transactionId: String, + val mac: Map<String, String>, + val keys: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000000000000000000000000000000000..6617b6b7c234f27c33aabeebd731e4f3de563e38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ + +internal interface VerificationInfoReady : VerificationInfo<ValidVerificationInfoReady> { + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List<String>? + + override fun asValidObject(): ValidVerificationInfoReady? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoReady( + validTransactionId, + validFromDevice, + validMethods + ) + } +} + +internal interface MessageVerificationReadyFactory { + fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..43843d8e299254ab68a167797d71be56e6335894 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest + +internal interface VerificationInfoRequest : VerificationInfo<ValidVerificationInfoRequest> { + + /** + * Required. The device ID which is initiating the request. + */ + val fromDevice: String? + + /** + * Required. The verification methods supported by the sender. + */ + val methods: List<String>? + + /** + * The POSIX timestamp in milliseconds for when the request was made. + * If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + * the message should be ignored by the receiver. + */ + val timestamp: Long? + + override fun asValidObject(): ValidVerificationInfoRequest? { + // FIXME No check on Timestamp? + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoRequest( + validTransactionId, + validFromDevice, + validMethods, + timestamp + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ac15a105674139645d49cbd397913f834be03cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS + +internal interface VerificationInfoStart : VerificationInfo<ValidVerificationInfoStart> { + + val method: String? + + /** + * Alice’s device ID + */ + val fromDevice: String? + + /** + * An array of key agreement protocols that Alice’s client understands. + * Must include “curve25519â€. + * Other methods may be defined in the future + */ + val keyAgreementProtocols: List<String>? + + /** + * An array of hashes that Alice’s client understands. + * Must include “sha256â€. Other methods may be defined in the future. + */ + val hashes: List<String>? + + /** + * An array of message authentication codes that Alice’s client understands. + * Must include “hkdf-hmac-sha256â€. + * Other methods may be defined in the future. + */ + val messageAuthenticationCodes: List<String>? + + /** + * An array of short authentication string methods that Alice’s client (and Alice) understands. + * Must include “decimalâ€. + * This document also describes the “emoji†method. + * Other methods may be defined in the future + */ + val shortAuthenticationStrings: List<String>? + + /** + * Shared secret, when starting verification with QR code + */ + val sharedSecret: String? + + fun toCanonicalJson(): String + + override fun asValidObject(): ValidVerificationInfoStart? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + + return when (method) { + VERIFICATION_METHOD_SAS -> { + val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null + val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null + val validMessageAuthenticationCodes = messageAuthenticationCodes + ?.takeIf { + it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256) + || it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF) + } + ?: return null + val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null + + ValidVerificationInfoStart.SasVerificationInfoStart( + validTransactionId, + validFromDevice, + validKeyAgreementProtocols, + validHashes, + validMessageAuthenticationCodes, + validShortAuthenticationStrings, + canonicalJson = toCanonicalJson() + ) + } + VERIFICATION_METHOD_RECIPROCATE -> { + val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null + + ValidVerificationInfoStart.ReciprocateVerificationInfoStart( + validTransactionId, + validFromDevice, + validSharedSecret + ) + } + else -> null + } + } +} + +sealed class ValidVerificationInfoStart( + open val transactionId: String, + open val fromDevice: String) { + data class SasVerificationInfoStart( + override val transactionId: String, + override val fromDevice: String, + val keyAgreementProtocols: List<String>, + val hashes: List<String>, + val messageAuthenticationCodes: List<String>, + val shortAuthenticationStrings: List<String>, + val canonicalJson: String + ) : ValidVerificationInfoStart(transactionId, fromDevice) + + data class ReciprocateVerificationInfoStart( + override val transactionId: String, + override val fromDevice: String, + val sharedSecret: String + ) : ValidVerificationInfoStart(transactionId, fromDevice) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c16fd970fb4cdfe086dff1afbf5dffcb0a9291a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import java.util.ArrayList +import javax.inject.Inject + +internal class VerificationMessageProcessor @Inject constructor( + private val cryptoService: CryptoService, + private val verificationService: DefaultVerificationService, + @UserId private val userId: String, + @DeviceId private val deviceId: String? +) : EventInsertLiveProcessor { + + private val transactionsHandledByOtherDevice = ArrayList<String>() + + private val allowedTypes = listOf( + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, + EventType.MESSAGE, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } + return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) + } + + override suspend fun process(realm: Realm, event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + + // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + // the message should be ignored by the receiver. + + if (!VerificationService.isValidRequest(event.ageLocalTs + ?: event.originServerTs)) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + // Relates to is not encrypted + val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.getClearType()) { + val msgType = event.getClearContent().toModel<MessageContent>()?.msgType + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel<MessageVerificationRequestContent>()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { + event.getClearContent().toModel<MessageVerificationStartContent>()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { + event.getClearContent().toModel<MessageVerificationReadyContent>()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { + relatesToEventId?.let { + transactionsHandledByOtherDevice.remove(it) + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + + Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") + return + } + + if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") + return + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + verificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) { + verificationService.onRoomRequestReceived(event) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt new file mode 100644 index 0000000000000000000000000000000000000000..ffe070993249f1fb47b044215809e5cea324d487 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState + +/** + * Verification can be performed using toDevice events or via DM. + * This class abstracts the concept of transport for verification + */ +internal interface VerificationTransport { + + /** + * Sends a message + */ + fun <T> sendToOther(type: String, + verificationInfo: VerificationInfo<T>, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) + + /** + * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success + */ + fun sendVerificationRequest(supportedMethods: List<String>, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List<String>?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) + + fun cancelTransaction(transactionId: String, + otherUserId: String, + otherUserDeviceId: String?, + code: CancelCode) + + fun done(transactionId: String, + onDone: (() -> Unit)?) + + /** + * Creates an accept message suitable for this transport + */ + fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>): VerificationInfoAccept + + fun createKey(tid: String, + pubKey: String): VerificationInfoKey + + /** + * Create start for SAS verification + */ + fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List<String>, + hashes: List<String>, + messageAuthenticationCodes: List<String>, + shortAuthenticationStrings: List<String>): VerificationInfoStart + + /** + * Create start for QR code verification + */ + fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart + + fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac + + fun createReady(tid: String, + fromDevice: String, + methods: List<String>): VerificationInfoReady + + // TODO Refactor + fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt new file mode 100644 index 0000000000000000000000000000000000000000..69f00ce35922dc6a990d5623acb9ea5456817337 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -0,0 +1,405 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import androidx.lifecycle.Observer +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.Operation +import androidx.work.WorkInfo +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.StringProvider +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class VerificationTransportRoomMessage( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + private val sessionId: String, + private val userId: String, + private val userDeviceId: String?, + private val roomId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val tx: DefaultVerificationTransaction?, + private val coroutineScope: CoroutineScope +) : VerificationTransport { + + override fun <T> sendToOther(type: String, + verificationInfo: VerificationInfo<T>, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = verificationInfo.toEventContent()!! + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + +// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> { +// override fun onSuccess(result: Operation.State.SUCCESS?) { +// if (onDone != null) { +// onDone() +// } else { +// tx?.state = nextState +// } +// } +// +// override fun onFailure(t: Throwable) { +// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") +// tx?.cancel(onErrorReason) +// } +// }, listenerExecutor) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + + val observer = object : Observer<List<WorkInfo>> { + override fun onChanged(workInfoList: List<WorkInfo>?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { wInfo -> + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } else { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun sendVerificationRequest(supportedMethods: List<String>, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List<String>?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + // This transport requires a room + requireNotNull(roomId) + + val validInfo = ValidVerificationInfoRequest( + transactionId = "", + fromDevice = userDeviceId ?: "", + methods = supportedMethods, + timestamp = System.currentTimeMillis() + ) + + val info = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = validInfo.fromDevice, + toUserId = otherUserId, + timestamp = validInfo.timestamp, + methods = validInfo.methods + ) + val content = info.toContent() + + val event = createEventAndLocalEcho( + localId, + EventType.MESSAGE, + roomId, + content + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + + workManagerProvider.workManager + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer<List<WorkInfo>> { + override fun onChanged(workInfoList: List<WorkInfo>?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == workRequest.id } + ?.let { wInfo -> + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { + callback(null, null) + } else { + val eventId = wInfo.outputData.getString(localId) + if (eventId != null) { + callback(eventId, validInfo) + } else { + callback(null, null) + } + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_CANCEL, + roomId = roomId, + content = MessageVerificationCancelContent.create(transactionId, code).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + enqueueSendWork(workerParams) + } + + override fun done(transactionId: String, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending done for $transactionId") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_DONE, + roomId = roomId, + content = MessageVerificationDoneContent( + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + val observer = object : Observer<List<WorkInfo>> { + override fun onChanged(workInfoList: List<WorkInfo>?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { _ -> + onDone?.invoke() + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> { + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + return workManagerProvider.workManager + .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() to workRequest.id + } + + private fun uniqueQueueName() = "${roomId}_VerificationWork" + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>) + : VerificationInfoAccept = MessageVerificationAcceptContent.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings + ) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List<String>, + hashes: List<String>, + messageAuthenticationCodes: List<String>, + shortAuthenticationStrings: List<String>): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + hashes, + keyAgreementProtocols, + messageAuthenticationCodes, + shortAuthenticationStrings, + VERIFICATION_METHOD_SAS, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionId + ), + null + ) + } + + override fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + null, + null, + null, + null, + VERIFICATION_METHOD_RECIPROCATE, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionId + ), + sharedSecret + ) + } + + override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = tid + ), + methods = methods + ) + } + + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ).also { + localEchoEventFactory.createLocalEcho(it) + } + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) { + // Not applicable (send event is called directly) + Timber.w("## SAS ignored verification ready with methods: ${keyReq.methods}") + } +} + +internal class VerificationTransportRoomMessageFactory @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + @SessionId + private val sessionId: String, + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val localEchoEventFactory: LocalEchoEventFactory, + private val taskExecutor: TaskExecutor +) { + + fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { + return VerificationTransportRoomMessage(workManagerProvider, + stringProvider, + sessionId, + userId, + deviceId, + roomId, + localEchoEventFactory, + tx, + taskExecutor.executorScope) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt new file mode 100644 index 0000000000000000000000000000000000000000..1dbcf31c78001d82f50852ccb1f9e24c576b5b95 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +internal class VerificationTransportToDevice( + private var tx: DefaultVerificationTransaction?, + private var sendToDeviceTask: SendToDeviceTask, + private val myDeviceId: String?, + private var taskExecutor: TaskExecutor +) : VerificationTransport { + + override fun sendVerificationRequest(supportedMethods: List<String>, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List<String>?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + val contentMap = MXUsersDevicesMap<Any>() + val validKeyReq = ValidVerificationInfoRequest( + transactionId = localId, + fromDevice = myDeviceId ?: "", + methods = supportedMethods, + timestamp = System.currentTimeMillis() + ) + val keyReq = KeyVerificationRequest( + fromDevice = validKeyReq.fromDevice, + methods = validKeyReq.methods, + timestamp = validKeyReq.timestamp, + transactionId = validKeyReq.transactionId + ) + toDevices?.forEach { + contentMap.setObject(otherUserId, it, keyReq) + } + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback.invoke(localId, validKeyReq) + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) { + Timber.d("## SAS sending verification ready with methods: ${keyReq.methods}") + val contentMap = MXUsersDevicesMap<Any>() + + contentMap.setObject(otherUserId, otherDeviceId, keyReq) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_READY, contentMap)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback?.invoke() + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun <T> sendToOther(type: String, + verificationInfo: VerificationInfo<T>, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val stateBeforeCall = tx?.state + val tx = tx ?: return + val contentMap = MXUsersDevicesMap<Any>() + val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() + ?: return Unit.also { tx.cancel() } + + contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.") + if (onDone != null) { + onDone() + } else { + // we may have received next state (e.g received accept in sending_start) + // We only put next state if the state was what is was before we started + if (tx.state == stateBeforeCall) { + tx.state = nextState + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") + tx.cancel(onErrorReason) + } + } + } + .executeBy(taskExecutor) + } + + override fun done(transactionId: String, onDone: (() -> Unit)?) { + val otherUserId = tx?.otherUserId ?: return + val otherUserDeviceId = tx?.otherDeviceId ?: return + val cancelMessage = KeyVerificationDone(transactionId) + val contentMap = MXUsersDevicesMap<Any>() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + onDone?.invoke() + Timber.v("## SAS verification [$transactionId] done") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to done.") + } + } + } + .executeBy(taskExecutor) + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap<Any>() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + } + } + .executeBy(taskExecutor) + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List<String>): VerificationInfoAccept = KeyVerificationAccept.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List<String>, + hashes: List<String>, + messageAuthenticationCodes: List<String>, + shortAuthenticationStrings: List<String>): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_SAS, + transactionId, + keyAgreementProtocols, + hashes, + messageAuthenticationCodes, + shortAuthenticationStrings, + null) + } + + override fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_RECIPROCATE, + transactionId, + null, + null, + null, + null, + sharedSecret) + } + + override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady { + return KeyVerificationReady( + transactionId = tid, + fromDevice = fromDevice, + methods = methods + ) + } +} + +internal class VerificationTransportToDeviceFactory @Inject constructor( + private val sendToDeviceTask: SendToDeviceTask, + @DeviceId val myDeviceId: String?, + private val taskExecutor: TaskExecutor) { + + fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { + return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..f0db2e0feeec729a0c1da8af69b9f6ac6261b310 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction +import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart +import org.matrix.android.sdk.internal.util.exhaustive +import timber.log.Timber + +internal class DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String?, + private val crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val cryptoStore: IMXCryptoStore, + // Not null only if other user is able to scan QR code + private val qrCodeData: QrCodeData?, + val userId: String, + val deviceId: String, + override val isIncoming: Boolean +) : DefaultVerificationTransaction( + setDeviceVerificationAction, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + userId, + transactionId, + otherUserId, + otherDeviceId, + isIncoming), + QrCodeVerificationTransaction { + + override val qrCodeText: String? + get() = qrCodeData?.toEncodedString() + + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + override fun userHasScannedOtherQrCode(otherQrCodeText: String) { + val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run { + Timber.d("## Verification QR: Invalid QR Code Data") + cancel(CancelCode.QrCodeInvalid) + return + } + + // Perform some checks + if (otherQrCodeData.transactionId != transactionId) { + Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId") + cancel(CancelCode.QrCodeInvalid) + return + } + + // check master key + val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey + var canTrustOtherUserMasterKey = false + + // Check the other device view of my MSK + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is. + // Let's check that it's correct + // If not -> Cancel + if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. + // Let's check that I see the same MSK + // If not -> Cancel + if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // I can trust the MSK then (i see the same one, and other session tell me it's trusted by him) + canTrustOtherUserMasterKey = true + } + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. + // Let's check that it's the good one + // If not -> Cancel + if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK + } + } + }.exhaustive + + val toVerifyDeviceIds = mutableListOf<String>() + + // Let's now check the other user/device key material + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // key1(aka userMasterCrossSigningPublicKey) is the MSK of the one displaying the QR code (i.e other user) + // Let's check that it matches what I think it should be + if (otherQrCodeData.userMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // It does so i should mark it as trusted + canTrustOtherUserMasterKey = true + Unit + } + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // key2 (aka otherDeviceKey) is my current device key in POV of the one displaying the QR code (i.e other device) + // Let's check that it's correct + if (otherQrCodeData.otherDeviceKey + != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { + Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit // Nothing special here, we will send a reciprocate start event, and then the other session will trust my device + // and thus allow me to request SSSS secret + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // key1 (aka otherDeviceKey) is the device key of the one displaying the QR code (i.e other device) + // Let's check that it matches what I have locally + if (otherQrCodeData.deviceKey + != cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) { + Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // Yes it does -> i should trust it and sign then upload the signature + toVerifyDeviceIds.add(otherDeviceId ?: "") + Unit + } + } + }.exhaustive + + if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { + // Nothing to verify + cancel(CancelCode.MismatchedKeys) + return + } + + // All checks are correct + // Send the shared secret so that sender can trust me + // qrCodeData.sharedSecret will be used to send the start request + start(otherQrCodeData.sharedSecret) + + trust( + canTrustOtherUserMasterKey = canTrustOtherUserMasterKey, + toVerifyDeviceIds = toVerifyDeviceIds.distinct(), + eventuallyMarkMyMasterKeyAsTrusted = true, + autoDone = false + ) + } + + private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) { + if (state != VerificationTxState.None) { + Timber.e("## Verification QR: start verification from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + state = VerificationTxState.Started + val startMessage = transport.createStartForQrCode( + deviceId, + transactionId, + remoteSecret + ) + + transport.sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.WaitingOtherReciprocateConfirm, + CancelCode.User, + onDone + ) + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + state = VerificationTxState.Cancelled(code, true) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) + } + + override fun isToDeviceTransport() = false + + // Other user has scanned our QR code. check that the secret matched, so we can trust him + fun onStartReceived(startReq: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { + if (qrCodeData == null) { + // Should not happen + cancel(CancelCode.UnexpectedMessage) + return + } + + if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) { + // Ok, we can trust the other user + // We can only trust the master key in this case + // But first, ask the user for a confirmation + state = VerificationTxState.QrScannedByOther + } else { + // Display a warning + cancel(CancelCode.MismatchedKeys) + } + } + + fun onDoneReceived() { + if (state != VerificationTxState.WaitingOtherReciprocateConfirm) { + cancel(CancelCode.UnexpectedMessage) + return + } + state = VerificationTxState.Verified + transport.done(transactionId) {} + } + + override fun otherUserScannedMyQrCode() { + when (qrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // Alice telling Bob that the code was scanned successfully is sufficient for Bob to trust Alice's key, + trust(true, emptyList(), false) + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // I now know that I have the correct device key for other session, + // and can sign it with the self-signing key and upload the signature + trust(false, listOf(otherDeviceId ?: ""), false) + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // I now know that i can trust my MSK + trust(true, emptyList(), true) + } + } + } + + override fun otherUserDidNotScannedMyQrCode() { + // What can I do then? + // At least remove the transaction... + cancel(CancelCode.MismatchedKeys) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e799f63ccf2193d754803e3fb0997c36b40eeba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.extensions.toUnsignedInt + +// MATRIX +private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1) + +fun QrCodeData.toEncodedString(): String { + var result = ByteArray(0) + + // MATRIX + for (i in prefix.indices) { + result += prefix[i] + } + + // Version + result += 2 + + // Mode + result += when (this) { + is QrCodeData.VerifyingAnotherUser -> 0 + is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1 + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2 + }.toByte() + + // TransactionId length + val length = transactionId.length + result += ((length and 0xFF00) shr 8).toByte() + result += length.toByte() + + // TransactionId + transactionId.forEach { + result += it.toByte() + } + + // Keys + firstKey.fromBase64().forEach { + result += it + } + secondKey.fromBase64().forEach { + result += it + } + + // Secret + sharedSecret.fromBase64().forEach { + result += it + } + + return result.toString(Charsets.ISO_8859_1) +} + +fun String.toQrCodeData(): QrCodeData? { + val byteArray = toByteArray(Charsets.ISO_8859_1) + + // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength + + // Check header + // MATRIX + if (byteArray.size < 10) return null + + for (i in prefix.indices) { + if (byteArray[i] != prefix[i]) { + return null + } + } + + var cursor = prefix.size // 6 + + // Version + if (byteArray[cursor] != 2.toByte()) { + return null + } + cursor++ + + // Get mode + val mode = byteArray[cursor].toInt() + cursor++ + + // Get transaction length, Big Endian format + val msb = byteArray[cursor].toUnsignedInt() + val lsb = byteArray[cursor + 1].toUnsignedInt() + + val transactionLength = msb.shl(8) + lsb + + cursor++ + cursor++ + + val secretLength = byteArray.size - 74 - transactionLength + + // ensure the secret length is 8 bytes min + if (secretLength < 8) { + return null + } + + val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1) + cursor += transactionLength + val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding() + + return when (mode) { + 0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret) + 1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret) + 2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret) + else -> null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ae0c136f4b671e99c38c5badb7dfa2c430f233e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format + */ +sealed class QrCodeData( + /** + * the event ID or transaction_id of the associated verification + */ + open val transactionId: String, + /** + * First key (32 bytes, in base64 no padding) + */ + val firstKey: String, + /** + * Second key (32 bytes, in base64 no padding) + */ + val secondKey: String, + /** + * a random shared secret (in base64 no padding) + */ + open val sharedSecret: String +) { + /** + * verifying another user with cross-signing + * QR code verification mode: 0x00 + */ + data class VerifyingAnotherUser( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other user's master cross-signing key is + */ + val otherUserMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherUserMasterCrossSigningPublicKey, + sharedSecret) + + /** + * self-verifying in which the current device does trust the master key + * QR code verification mode: 0x01 + */ + data class SelfVerifyingMasterKeyTrusted( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other device's device key is + */ + val otherDeviceKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherDeviceKey, + sharedSecret) + + /** + * self-verifying in which the current device does not yet trust the master key + * QR code verification mode: 0x02 + */ + data class SelfVerifyingMasterKeyNotTrusted( + override val transactionId: String, + /** + * the current device's device key + */ + val deviceKey: String, + /** + * what the device thinks the user's master cross-signing key is + */ + val userMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + deviceKey, + userMasterCrossSigningPublicKey, + sharedSecret) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt new file mode 100644 index 0000000000000000000000000000000000000000..edff10382011b6072a1daf8852c92d12f402b39b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import java.security.SecureRandom + +fun generateSharedSecretV2(): String { + val secureRandom = SecureRandom() + + // 8 bytes long + val secretBytes = ByteArray(8) + secureRandom.nextBytes(secretBytes) + return secretBytes.toBase64NoPadding() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..c633dc5860a60f7827f8117ea90cee0532e69d83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database + +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import timber.log.Timber + +suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) { + Realm.getInstance(config).use { bgRealm -> + bgRealm.beginTransaction() + val result: T + try { + val start = System.currentTimeMillis() + result = transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + val end = System.currentTimeMillis() + val time = end - start + Timber.v("Execute transaction in $time millis") + } + } finally { + if (bgRealm.isInTransaction) { + bgRealm.cancelTransaction() + } + } + result + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f735eb558d92024afea09f47cff57eea44c28a2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +internal object DBConstants { + + const val STATE_EVENTS_CHUNK_TOKEN = "STATE_EVENTS_CHUNK_TOKEN" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt new file mode 100644 index 0000000000000000000000000000000000000000..d12f8628b197017595dce7ff50e1696c31c5cb13 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.task.TaskExecutor +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L +private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 + +/** + * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events + * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold. + * We make sure to still have a minimum number of events so it's not becoming unusable. + * So this won't work for users with a big number of very active rooms. + */ +internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { + + override fun onStart() { + taskExecutor.executorScope.launch(Dispatchers.Default) { + awaitTransaction(realmConfiguration) { realm -> + val allRooms = realm.where(RoomEntity::class.java).findAll() + Timber.v("There are ${allRooms.size} rooms in this session") + cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) + } + } + } + + private suspend fun cleanUp(realm: Realm, threshold: Long) { + val numberOfEvents = realm.where(EventEntity::class.java).findAll().size + val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size + Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") + if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) { + Timber.v("Db is low enough") + } else { + val thresholdChunks = realm.where(ChunkEntity::class.java) + .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold) + .findAll() + + Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events") + for (chunk in thresholdChunks) { + val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) + val thresholdDisplayIndex = maxDisplayIndex - threshold + val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() + Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") + chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size + eventsToRemove.forEach { + val canDeleteRoot = it.root?.stateKey == null + if (canDeleteRoot) { + it.root?.deleteFromRealm() + } + it.readReceipts?.readReceipts?.deleteAllFromRealm() + it.readReceipts?.deleteFromRealm() + it.annotations?.apply { + editSummary?.deleteFromRealm() + pollResponseSummary?.deleteFromRealm() + referencesSummaryEntity?.deleteFromRealm() + reactionsSummary.deleteAllFromRealm() + } + it.annotations?.deleteFromRealm() + it.readReceipts?.deleteFromRealm() + it.deleteFromRealm() + } + // We reset the prevToken so we will need to fetch again. + chunk.prevToken = null + } + cleanUp(realm, (threshold / 1.5).toLong()) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..1834961ff2635ffed8f1f38c9d42e98116de3ac5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.RealmConfiguration +import io.realm.RealmResults +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, + private val cryptoService: CryptoService) + : RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) { + + override val query = Monarchy.Query<EventInsertEntity> { + it.where(EventInsertEntity::class.java) + } + + override fun onChange(results: RealmResults<EventInsertEntity>) { + if (!results.isLoaded || results.isEmpty()) { + return + } + val idsToDeleteAfterProcess = ArrayList<String>() + val filteredEvents = ArrayList<EventInsertEntity>(results.size) + Timber.v("EventInsertEntity updated with ${results.size} results in db") + results.forEach { + if (shouldProcess(it)) { + // don't use copy from realm over there + val copiedEvent = EventInsertEntity( + eventId = it.eventId, + eventType = it.eventType + ).apply { + insertType = it.insertType + } + filteredEvents.add(copiedEvent) + } + idsToDeleteAfterProcess.add(it.eventId) + } + observerScope.launch { + awaitTransaction(realmConfiguration) { realm -> + Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") + filteredEvents.forEach { eventInsert -> + val eventId = eventInsert.eventId + val event = EventEntity.where(realm, eventId).findFirst() + if (event == null) { + Timber.v("Event $eventId not found") + return@forEach + } + val domainEvent = event.asDomain() + decryptIfNeeded(domainEvent) + processors.filter { + it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) + }.forEach { + it.process(realm, domainEvent) + } + } + realm.where(EventInsertEntity::class.java) + .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) + .findAll() + .deleteAllFromRealm() + } + } + } + + private fun decryptIfNeeded(event: Event) { + if (event.isEncrypted() && event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId ?: "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.v("Failed to decrypt event") + // TODO -> we should keep track of this and retry, or some processing will never be handled + } + } + } + + private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { + return processors.any { + it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..453cbae325277007953b958a6a256784d4b739e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database + +import android.content.Context +import android.util.Base64 +import androidx.core.content.edit +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils +import io.realm.RealmConfiguration +import timber.log.Timber +import java.security.SecureRandom +import javax.inject.Inject + +/** + * On creation a random key is generated, this key is then encrypted using the system KeyStore. + * The encrypted key is stored in shared preferences. + * When the database is opened again, the encrypted key is taken from the shared pref, + * then the Keystore is used to decrypt the key. The decrypted key is passed to the RealConfiguration. + * + * On android >=M, the KeyStore generates an AES key to encrypt/decrypt the database key, + * and the encrypted key is stored with the initialization vector in base64 in the shared pref. + * On android <M, the KeyStore cannot create AES keys, so a public/private key pair is generated, + * then we generate a random secret key. The database key is encrypted with the secret key; The secret + * key is encrypted with the public RSA key and stored with the encrypted key in the shared pref + */ +internal class RealmKeysUtils @Inject constructor(context: Context, + private val secretStoringUtils: SecretStoringUtils) { + + private val rng = SecureRandom() + + // Keep legacy preferences name for compatibility reason + private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE) + + private fun generateKeyForRealm(): ByteArray { + val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH) + rng.nextBytes(keyForRealm) + return keyForRealm + } + + /** + * Check if there is already a key for this alias + */ + private fun hasKeyForDatabase(alias: String): Boolean { + return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias") + } + + /** + * Creates a new secure random key for this database. + * The random key is then encrypted by the keystore, and the encrypted key is stored + * in shared preferences. + * + * @return the generated key (can be passed to Realm Configuration) + */ + private fun createAndSaveKeyForDatabase(alias: String): ByteArray { + val key = generateKeyForRealm() + val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) + val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) + sharedPreferences.edit { + putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) + } + return key + } + + /** + * Retrieves the key for this database + * throws if something goes wrong + */ + private fun extractKeyForDatabase(alias: String): ByteArray { + val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) + val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) + val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias) + return Base64.decode(b64!!, Base64.NO_PADDING) + } + + fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) { + val key = getRealmEncryptionKey(alias) + + realmConfigurationBuilder.encryptionKey(key) + } + + // Expose to handle Realm migration to riotX + fun getRealmEncryptionKey(alias: String) : ByteArray { + val key = if (hasKeyForDatabase(alias)) { + Timber.i("Found key for alias:$alias") + extractKeyForDatabase(alias) + } else { + Timber.i("Create key for DB alias:$alias") + createAndSaveKeyForDatabase(alias) + } + + if (BuildConfig.LOG_PRIVATE_DATA) { + val log = key.joinToString("") { "%02x".format(it) } + Timber.w("Database key for alias `$alias`: $log") + } + + return key + } + + // Delete elements related to the alias + fun clear(alias: String) { + if (hasKeyForDatabase(alias)) { + secretStoringUtils.safeDeleteKey(alias) + + sharedPreferences.edit { + remove("${ENCRYPTED_KEY_PREFIX}_$alias") + } + } + } + + companion object { + private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..adf77840aeb678f14acd59f4d6899c5eb6ba2f23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmConfiguration +import io.realm.RealmObject +import io.realm.RealmResults +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +internal interface LiveEntityObserver : SessionLifecycleObserver + +internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration) + : LiveEntityObserver, RealmChangeListener<RealmResults<T>> { + + private companion object { + val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") + } + + protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher()) + protected abstract val query: Monarchy.Query<T> + private val isStarted = AtomicBoolean(false) + private val backgroundRealm = AtomicReference<Realm>() + private lateinit var results: AtomicReference<RealmResults<T>> + + override fun onStart() { + if (isStarted.compareAndSet(false, true)) { + BACKGROUND_HANDLER.post { + val realm = Realm.getInstance(realmConfiguration) + backgroundRealm.set(realm) + val queryResults = query.createQuery(realm).findAll() + queryResults.addChangeListener(this) + results = AtomicReference(queryResults) + } + } + } + + override fun onStop() { + if (isStarted.compareAndSet(true, false)) { + BACKGROUND_HANDLER.post { + results.getAndSet(null).removeAllChangeListeners() + backgroundRealm.getAndSet(null).also { + it.close() + } + observerScope.coroutineContext.cancelChildren() + } + } + } + + override fun onClearCache() { + observerScope.coroutineContext.cancelChildren() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt new file mode 100644 index 0000000000000000000000000000000000000000..712b01a69a353d2a409dd6216e5c91f1e6dd7055 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +internal suspend fun <T> awaitNotEmptyResult(realmConfiguration: RealmConfiguration, + timeoutMillis: Long, + builder: (Realm) -> RealmQuery<T>) { + withTimeout(timeoutMillis) { + // Confine Realm interaction to a single thread with Looper. + withContext(Dispatchers.Main) { + val latch = CompletableDeferred<Unit>() + + Realm.getInstance(realmConfiguration).use { realm -> + val result = builder(realm).findAllAsync() + + val listener = object : RealmChangeListener<RealmResults<T>> { + override fun onChange(it: RealmResults<T>) { + if (it.isNotEmpty()) { + result.removeChangeListener(this) + latch.complete(Unit) + } + } + } + + result.addChangeListener(listener) + try { + latch.await() + } catch (e: CancellationException) { + result.removeChangeListener(listener) + throw e + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt new file mode 100644 index 0000000000000000000000000000000000000000..195116cfe6cc032633365ccf203a4ab8d2ae91e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber +import javax.inject.Inject + +class RealmSessionStoreMigration @Inject constructor() : RealmMigration { + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Session from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + // Add hasFailedSending in RoomSummary and a small warning icon on room list + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.HAS_FAILED_SENDING, Boolean::class.java) + ?.transform { obj -> + obj.setBoolean(RoomSummaryEntityFields.HAS_FAILED_SENDING, false) + } + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, Boolean::class.java) + ?.transform { obj -> + obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..e53240c5b86b3cdd94e77f898e2a90ee7c2857f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.core.content.edit +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.session.SessionModule +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +private const val REALM_SHOULD_CLEAR_FLAG_ = "REALM_SHOULD_CLEAR_FLAG_" +private const val REALM_NAME = "disk_store.realm" + +/** + * This class is handling creation of RealmConfiguration for a session. + * It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys. + * It's clearly not perfect but there is no way to catch the native crash. + */ +internal class SessionRealmConfigurationFactory @Inject constructor( + private val realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory val directory: File, + @SessionId val sessionId: String, + @UserMd5 val userMd5: String, + val migration: RealmSessionStoreMigration, + context: Context) { + + companion object { + const val SESSION_STORE_SCHEMA_VERSION = 2L + } + + // Keep legacy preferences name for compatibility reason + private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) + + fun create(): RealmConfiguration { + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) + if (shouldClearRealm) { + Timber.v("************************************************************") + Timber.v("The realm file session was corrupted and couldn't be loaded.") + Timber.v("The file has been deleted to recover.") + Timber.v("************************************************************") + deleteRealmFiles() + } + sharedPreferences.edit { + putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) + } + + val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() + .directory(directory) + .name(REALM_NAME) + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(SessionRealmModule()) + .schemaVersion(SESSION_STORE_SCHEMA_VERSION) + .migration(migration) + .build() + + // Try creating a realm instance and if it succeeds we can clear the flag + Realm.getInstance(realmConfiguration).use { + Timber.v("Successfully create realm instance") + sharedPreferences.edit { + putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) + } + } + return realmConfiguration + } + + // Delete all the realm files of the session + private fun deleteRealmFiles() { + listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file -> + try { + File(directory, file).deleteRecursively() + } catch (e: Exception) { + Timber.e(e, "Unable to delete files") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..afe228a240033c0877c93a45ccc577a716f5931c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.extensions.assertIsManaged +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.createObject +import timber.log.Timber + +internal fun ChunkEntity.deleteOnCascade() { + assertIsManaged() + this.timelineEvents.deleteAllFromRealm() + this.deleteFromRealm() +} + +internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { + assertIsManaged() + val localRealm = this.realm + val eventsToMerge: List<TimelineEventEntity> + if (direction == PaginationDirection.FORWARDS) { + this.nextToken = chunkToMerge.nextToken + this.isLastForward = chunkToMerge.isLastForward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + } else { + this.prevToken = chunkToMerge.prevToken + this.isLastBackward = chunkToMerge.isLastBackward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + } + chunkToMerge.stateEvents.forEach { stateEvent -> + addStateEvent(roomId, stateEvent, direction) + } + eventsToMerge.forEach { + addTimelineEventFromMerge(localRealm, it, direction) + } +} + +internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { + if (direction == PaginationDirection.BACKWARDS) { + Timber.v("We don't keep chunk state events when paginating backward") + } else { + val stateKey = stateEvent.stateKey ?: return + val type = stateEvent.type + val pastStateEvent = stateEvents.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) + .equalTo(EventEntityFields.STATE_KEY, stateKey) + .equalTo(CurrentStateEventEntityFields.TYPE, type) + .findFirst() + + if (pastStateEvent != null) { + stateEvents.remove(pastStateEvent) + } + stateEvents.add(stateEvent) + } +} + +internal fun ChunkEntity.addTimelineEvent(roomId: String, + eventEntity: EventEntity, + direction: PaginationDirection, + roomMemberContentsByUser: Map<String, RoomMemberContent?>) { + val eventId = eventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val senderId = eventEntity.sender ?: "" + + // Update RR for the sender of a new message with a dummy one + val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId) + val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply { + this.localId = localId + this.root = eventEntity + this.eventId = eventId + this.roomId = roomId + this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + this.readReceipts = readReceiptsSummaryEntity + this.displayIndex = displayIndex + val roomMemberContent = roomMemberContentsByUser[senderId] + this.senderAvatar = roomMemberContent?.avatarUrl + this.senderName = roomMemberContent?.displayName + isUniqueDisplayName = if (roomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser) + } else { + true + } + } + numberOfTimelineEvents++ + timelineEvents.add(timelineEventEntity) +} + +private fun computeIsUnique( + realm: Realm, + roomId: String, + isLastForward: Boolean, + senderRoomMemberContent: RoomMemberContent, + roomMemberContentsByUser: Map<String, RoomMemberContent?> +): Boolean { + val isHistoricalUnique = roomMemberContentsByUser.values.find { + it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName + } == null + return if (isLastForward) { + val isLiveUnique = RoomMemberSummaryEntity + .where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName) + .findAll() + .none { + !roomMemberContentsByUser.containsKey(it.userId) + } + isHistoricalUnique && isLiveUnique + } else { + isHistoricalUnique + } +} + +private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) { + val eventId = timelineEventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val copied = realm.createObject<TimelineEventEntity>().apply { + this.localId = localId + this.root = timelineEventEntity.root + this.eventId = timelineEventEntity.eventId + this.roomId = timelineEventEntity.roomId + this.annotations = timelineEventEntity.annotations + this.readReceipts = timelineEventEntity.readReceipts + this.displayIndex = displayIndex + this.senderAvatar = timelineEventEntity.senderAvatar + this.senderName = timelineEventEntity.senderName + this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName + } + timelineEvents.add(copied) +} + +private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { + val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() + ?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply { + this.roomId = roomId + } + val originServerTs = eventEntity.originServerTs + if (originServerTs != null) { + val timestampOfEvent = originServerTs.toDouble() + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + // If the synced RR is older, update + if (timestampOfEvent > readReceiptOfSender.originServerTs) { + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() + readReceiptOfSender.eventId = eventEntity.eventId + readReceiptOfSender.originServerTs = timestampOfEvent + previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) + readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) + } + } + return readReceiptsSummaryEntity +} + +internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { + return when (direction) { + PaginationDirection.FORWARDS -> { + (timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1 + } + PaginationDirection.BACKWARDS -> { + (timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..4874a1742baefa7c2df4b4ee54e3a59b54dff07e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity + +internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { + chunks.remove(chunkEntity) + chunkEntity.deleteOnCascade() +} + +internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { + if (!chunks.contains(chunkEntity)) { + chunks.add(chunkEntity) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..95ae59f80f0a53482dd2262fdc6d91849ea4a430 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.extensions.assertIsManaged +import io.realm.Realm + +internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { + val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) + return if (currentIdNum == null) { + 1 + } else { + currentIdNum.toLong() + 1 + } +} + +internal fun TimelineEventEntity.deleteOnCascade() { + assertIsManaged() + root?.deleteFromRealm() + annotations?.deleteFromRealm() + readReceipts?.deleteFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..9811afab8e628c13ad650eebf19e4fd5c11a4b34 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import javax.inject.Inject + +internal class AccountDataMapper @Inject constructor(moshi: Moshi) { + + private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(entity: UserAccountDataEntity): UserAccountDataEvent { + return UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab094e94b8d0221dc888008f1454a44cdd13f76c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal object ContentMapper { + + private val moshi = MoshiProvider.providesMoshi() + private val adapter = moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(content: String?): Content? { + return content?.let { + adapter.fromJson(it) + } + } + + fun map(content: Content?): String? { + return content?.let { + adapter.toJson(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc22c7ed3caedbdb2302b23a253cd7fbe270a20c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.internal.database.model.DraftEntity + +/** + * DraftEntity <-> UserDraft + */ +internal object DraftMapper { + + fun map(entity: DraftEntity): UserDraft { + return when (entity.draftMode) { + DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) + DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) + DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) + DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + else -> null + } ?: UserDraft.REGULAR("") + } + + fun map(domain: UserDraft): DraftEntity { + return when (domain) { + is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") + is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f697d53ca98a124f79966c2aabc7d9695ed574d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import io.realm.RealmList + +internal object EventAnnotationsSummaryMapper { + fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary { + return EventAnnotationsSummary( + eventId = annotationsSummary.eventId, + reactionsSummary = annotationsSummary.reactionsSummary.toList().map { + ReactionAggregatedSummary( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) + }, + editSummary = annotationsSummary.editSummary?.let { + EditAggregatedSummary( + ContentMapper.map(it.aggregatedContent), + it.sourceEvents.toList(), + it.sourceLocalEchoEvents.toList(), + it.lastEditTs + ) + }, + referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { + ReferencesAggregatedSummary( + it.eventId, + ContentMapper.map(it.content), + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) + }, + pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) + } + + ) + } + + fun map(annotationsSummary: EventAnnotationsSummary, roomId: String): EventAnnotationsSummaryEntity { + val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity() + eventAnnotationsSummaryEntity.eventId = annotationsSummary.eventId + eventAnnotationsSummaryEntity.roomId = roomId + eventAnnotationsSummaryEntity.editSummary = annotationsSummary.editSummary?.let { + EditAggregatedSummaryEntity( + ContentMapper.map(it.aggregatedContent), + RealmList<String>().apply { addAll(it.sourceEvents) }, + RealmList<String>().apply { addAll(it.localEchos) }, + it.lastEditTs + ) + } + eventAnnotationsSummaryEntity.reactionsSummary = annotationsSummary.reactionsSummary.let { + RealmList<ReactionAggregatedSummaryEntity>().apply { + addAll(it.map { + ReactionAggregatedSummaryEntity( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + RealmList<String>().apply { addAll(it.sourceEvents) }, + RealmList<String>().apply { addAll(it.localEchoEvents) } + ) + }) + } + } + eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let { + ReferencesAggregatedSummaryEntity( + it.eventId, + ContentMapper.map(it.content), + RealmList<String>().apply { addAll(it.sourceEvents) }, + RealmList<String>().apply { addAll(it.localEchos) } + ) + } + eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) + } + return eventAnnotationsSummaryEntity + } +} + +internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary { + return EventAnnotationsSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..61f09dcececfdb497462f8a89797e0e69ac080c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.JsonDataException +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber + +internal object EventMapper { + + fun map(event: Event, roomId: String): EventEntity { + val uds = if (event.unsignedData == null) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData) + val eventEntity = EventEntity() + // TODO change this as we shouldn't use event everywhere + eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" + eventEntity.roomId = event.roomId ?: roomId + eventEntity.content = ContentMapper.map(event.content) + eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent()) + eventEntity.isUseless = IsUselessResolver.isUseless(event) + eventEntity.stateKey = event.stateKey + eventEntity.type = event.type + eventEntity.sender = event.senderId + eventEntity.originServerTs = event.originServerTs + eventEntity.redacts = event.redacts + eventEntity.age = event.unsignedData?.age ?: event.originServerTs + eventEntity.unsignedData = uds + eventEntity.decryptionResultJson = event.mxDecryptionResult?.let { + MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it) + } + eventEntity.decryptionErrorReason = event.mCryptoErrorReason + eventEntity.decryptionErrorCode = event.mCryptoError?.name + return eventEntity + } + + fun map(eventEntity: EventEntity): Event { + val ud = eventEntity.unsignedData + ?.takeIf { it.isNotBlank() } + ?.let { + try { + MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(it) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse UnsignedData") + null + } + } + + return Event( + type = eventEntity.type, + eventId = eventEntity.eventId, + content = ContentMapper.map(eventEntity.content), + prevContent = ContentMapper.map(eventEntity.prevContent), + originServerTs = eventEntity.originServerTs, + senderId = eventEntity.sender, + stateKey = eventEntity.stateKey, + roomId = eventEntity.roomId, + unsignedData = ud, + redacts = eventEntity.redacts + ).also { + it.ageLocalTs = eventEntity.ageLocalTs + it.sendState = eventEntity.sendState + eventEntity.decryptionResultJson?.let { json -> + try { + it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse decryption result") + } + } + // TODO get the full crypto error object + it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode -> + MXCryptoError.ErrorType.valueOf(errorCode) + } + it.mCryptoErrorReason = eventEntity.decryptionErrorReason + } + } +} + +internal fun EventEntity.asDomain(): Event { + return EventMapper.map(this) +} + +internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { + return EventMapper.map(this, roomId).apply { + this.sendState = sendState + this.ageLocalTs = ageLocalTs + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..09c96215b4b0c060d9c17aa6f951668db0017431 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity + +internal object GroupSummaryMapper { + + fun map(groupSummaryEntity: GroupSummaryEntity): GroupSummary { + return GroupSummary( + groupSummaryEntity.groupId, + groupSummaryEntity.membership, + groupSummaryEntity.displayName, + groupSummaryEntity.shortDescription, + groupSummaryEntity.avatarUrl, + groupSummaryEntity.roomIds.toList(), + groupSummaryEntity.userIds.toList() + ) + } +} + +internal fun GroupSummaryEntity.asDomain(): GroupSummary { + return GroupSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d38c3fbe54f98e45f52ba0dd7046070af6db25f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity + +/** + * HomeServerCapabilitiesEntity -> HomeSeverCapabilities + */ +internal object HomeServerCapabilitiesMapper { + + fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { + return HomeServerCapabilities( + canChangePassword = entity.canChangePassword, + maxUploadFileSize = entity.maxUploadFileSize, + lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, + defaultIdentityServerUrl = entity.defaultIdentityServerUrl, + adminE2EByDefault = entity.adminE2EByDefault + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..5dde01e15cf1b66b4ee72d67304db6fd886b33a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent + +internal object IsUselessResolver { + + /** + * @return true if the event is useless + */ + fun isUseless(event: Event): Boolean { + return when (event.type) { + EventType.STATE_ROOM_MEMBER -> { + // Call toContent(), to filter out null value + event.content != null + && event.content.toContent() == event.resolvedPrevContent()?.toContent() + } + else -> false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..18c774ac40669d60a61f53a13fbf23582e89fab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import io.realm.RealmList + +internal object PollResponseAggregatedSummaryEntityMapper { + + fun map(entity: PollResponseAggregatedSummaryEntity): PollResponseAggregatedSummary { + return PollResponseAggregatedSummary( + aggregatedContent = ContentMapper.map(entity.aggregatedContent).toModel(), + closedTime = entity.closedTime, + localEchos = entity.sourceLocalEchoEvents.toList(), + sourceEvents = entity.sourceEvents.toList(), + nbOptions = entity.nbOptions + ) + } + + fun map(model: PollResponseAggregatedSummary): PollResponseAggregatedSummaryEntity { + return PollResponseAggregatedSummaryEntity( + aggregatedContent = ContentMapper.map(model.aggregatedContent.toContent()), + nbOptions = model.nbOptions, + closedTime = model.closedTime, + sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) }, + sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) } + ) + } +} + +internal fun PollResponseAggregatedSummaryEntity.asDomain(): PollResponseAggregatedSummary { + return PollResponseAggregatedSummaryEntityMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..cce780bad87208891cc2e3879baf6495b588d0ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.internal.database.model.PushConditionEntity + +internal object PushConditionMapper { + + fun map(entity: PushConditionEntity): PushCondition { + return PushCondition( + kind = entity.kind, + iz = entity.iz, + key = entity.key, + pattern = entity.pattern + ) + } + + fun map(domain: PushCondition): PushConditionEntity { + return PushConditionEntity( + kind = domain.kind, + iz = domain.iz, + key = domain.key, + pattern = domain.pattern + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..90fc62f8f37f659d87397e9c8adc8f3394c32cc5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmList +import timber.log.Timber + +internal object PushRulesMapper { + + private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java)) + +// private val listOfAnyAdapter: JsonAdapter<List<Any>> = +// moshi.adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java), kotlin.collections.emptySet(), "actions") + + fun mapContentRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern) + ) + ) + } + + private fun fromActionStr(actionsStr: String?): List<Any> { + try { + return actionsStr?.let { moshiActionsAdapter.fromJson(it) }.orEmpty() + } catch (e: Throwable) { + Timber.e(e, "## failed to map push rule actions <$actionsStr>") + return emptyList() + } + } + + fun mapRoomRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId) + ) + ) + } + + fun mapSenderRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId) + ) + ) + } + + fun map(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = pushrule.conditions?.map { PushConditionMapper.map(it) } + ) + } + + fun map(pushRule: PushRule): PushRuleEntity { + return PushRuleEntity( + actionsStr = moshiActionsAdapter.toJson(pushRule.actions), + default = pushRule.default ?: false, + enabled = pushRule.enabled, + ruleId = pushRule.ruleId, + pattern = pushRule.pattern, + conditions = pushRule.conditions?.let { + RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray()) + } ?: RealmList() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..9912bcd4f60ad022318333b889e37d1a5caee4dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.pushers.PusherData +import org.matrix.android.sdk.internal.database.model.PusherDataEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.session.pushers.JsonPusher + +internal object PushersMapper { + + fun map(pushEntity: PusherEntity): Pusher { + return Pusher( + pushKey = pushEntity.pushKey, + kind = pushEntity.kind ?: "", + appId = pushEntity.appId, + appDisplayName = pushEntity.appDisplayName, + deviceDisplayName = pushEntity.deviceDisplayName, + profileTag = pushEntity.profileTag, + lang = pushEntity.lang, + data = PusherData(pushEntity.data?.url, pushEntity.data?.format), + state = pushEntity.state + ) + } + + fun map(pusher: JsonPusher): PusherEntity { + return PusherEntity( + pushKey = pusher.pushKey, + kind = pusher.kind, + appId = pusher.appId, + appDisplayName = pusher.appDisplayName, + deviceDisplayName = pusher.deviceDisplayName, + profileTag = pusher.profileTag, + lang = pusher.lang, + data = PusherDataEntity(pusher.data?.url, pusher.data?.format) + ) + } +} + +internal fun PusherEntity.asDomain(): Pusher { + return PushersMapper.map(this) +} + +internal fun JsonPusher.toEntity(): PusherEntity { + return PushersMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..188ca4937c47d758f8e8f709619bfd966d561bce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { + + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> { + if (readReceiptsSummaryEntity == null) { + return emptyList() + } + return Realm.getInstance(realmConfiguration).use { realm -> + val readReceipts = readReceiptsSummaryEntity.readReceipts + readReceipts + .mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..65ea7fa7c6c540412cc816d28a0d6630136a0a59 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity + +internal object RoomMemberSummaryMapper { + + fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary { + return RoomMemberSummary( + userId = roomMemberSummaryEntity.userId, + avatarUrl = roomMemberSummaryEntity.avatarUrl, + displayName = roomMemberSummaryEntity.displayName, + membership = roomMemberSummaryEntity.membership + ) + } +} + +internal fun RoomMemberSummaryEntity.asDomain(): RoomMemberSummary { + return RoomMemberSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd2aba3e544cc49e195f473504e9add51d82315a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import javax.inject.Inject + +internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper, + private val typingUsersTracker: DefaultTypingUsersTracker) { + + fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { + val tags = roomSummaryEntity.tags.map { + RoomTag(it.tagName, it.tagOrder) + } + + val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { + timelineEventMapper.map(it, buildReadReceipts = false) + } + // typings are updated through the sync where room summary entity gets updated no matter what, so it's ok get there + val typingUsers = typingUsersTracker.getTypingUsers(roomSummaryEntity.roomId) + + return RoomSummary( + roomId = roomSummaryEntity.roomId, + displayName = roomSummaryEntity.displayName ?: "", + name = roomSummaryEntity.name ?: "", + topic = roomSummaryEntity.topic ?: "", + avatarUrl = roomSummaryEntity.avatarUrl ?: "", + isDirect = roomSummaryEntity.isDirect, + latestPreviewableEvent = latestEvent, + joinedMembersCount = roomSummaryEntity.joinedMembersCount, + invitedMembersCount = roomSummaryEntity.invitedMembersCount, + otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), + highlightCount = roomSummaryEntity.highlightCount, + notificationCount = roomSummaryEntity.notificationCount, + hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, + tags = tags, + typingUsers = typingUsers, + membership = roomSummaryEntity.membership, + versioningState = roomSummaryEntity.versioningState, + readMarkerId = roomSummaryEntity.readMarkerId, + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(), + canonicalAlias = roomSummaryEntity.canonicalAlias, + aliases = roomSummaryEntity.aliases.toList(), + isEncrypted = roomSummaryEntity.isEncrypted, + encryptionEventTs = roomSummaryEntity.encryptionEventTs, + breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, + roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, + inviterId = roomSummaryEntity.inviterId, + hasFailedSending = roomSummaryEntity.hasFailedSending + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..71c586cffe2f227b4d1ef541b20b1e2398945246 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import javax.inject.Inject + +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { + + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent { + val readReceipts = if (buildReadReceipts) { + correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + } else { + null + } + return TimelineEvent( + root = timelineEventEntity.root?.asDomain() + ?: Event("", timelineEventEntity.eventId), + eventId = timelineEventEntity.eventId, + annotations = timelineEventEntity.annotations?.asDomain(), + localId = timelineEventEntity.localId, + displayIndex = timelineEventEntity.displayIndex, + senderInfo = SenderInfo( + userId = timelineEventEntity.root?.sender ?: "", + displayName = timelineEventEntity.senderName, + isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, + avatarUrl = timelineEventEntity.senderAvatar + ), + readReceipts = readReceipts + ?.distinctBy { + it.user + }?.sortedByDescending { + it.originServerTs + }.orEmpty() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f5c5415852fa038dfafe65374ba5fae50cde733 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.internal.database.model.UserEntity + +internal object UserMapper { + + fun map(userEntity: UserEntity): User { + return User( + userEntity.userId, + userEntity.displayName, + userEntity.avatarUrl + ) + } +} + +internal fun UserEntity.asDomain(): User { + return UserMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..94306fadc8fec3f57f50faf2e00778c0cf13f575 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class BreadcrumbsEntity( + var recentRoomIds: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1f7fda7cf51eb45a913ed6a1a3e6c8a16340e63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Index +import io.realm.annotations.LinkingObjects + +internal open class ChunkEntity(@Index var prevToken: String? = null, + // Because of gaps we can have several chunks with nextToken == null + @Index var nextToken: String? = null, + var stateEvents: RealmList<EventEntity> = RealmList(), + var timelineEvents: RealmList<TimelineEventEntity> = RealmList(), + var numberOfTimelineEvents: Long = 0, + // Only one chunk will have isLastForward == true + @Index var isLastForward: Boolean = false, + @Index var isLastBackward: Boolean = false +) : RealmObject() { + + fun identifier() = "${prevToken}_$nextToken" + + // If true, then this chunk was previously a last forward chunk + fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward + + @LinkingObjects("chunks") + val room: RealmResults<RoomEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..bdd86cec7b9163329c07aa0d8d9f17096c60018d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class CurrentStateEventEntity(var eventId: String = "", + var root: EventEntity? = null, + @Index var roomId: String = "", + @Index var type: String = "", + @Index var stateKey: String = "" +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7254e6241dff0c72f3ed1c4b3f1c94ec5df1b9b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class DraftEntity(var content: String = "", + var draftMode: String = MODE_REGULAR, + var linkedEventId: String = "" + +) : RealmObject() { + + companion object { + const val MODE_REGULAR = "REGULAR" + const val MODE_EDIT = "EDIT" + const val MODE_REPLY = "REPLY" + const val MODE_QUOTE = "QUOTE" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f98d2218a76a8292f584ee37f3d0fcf2a111e78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of edition of a message + */ +internal open class EditAggregatedSummaryEntity( + var aggregatedContent: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList<String> = RealmList(), + var sourceLocalEchoEvents: RealmList<String> = RealmList(), + var lastEditTs: Long = 0 +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..140058fbafcbc939c4677b09aa45380c4fbb6ee5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class EventAnnotationsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String? = null, + var reactionsSummary: RealmList<ReactionAggregatedSummaryEntity> = RealmList(), + var editSummary: EditAggregatedSummaryEntity? = null, + var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null, + var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c76e1402ac13acb14d700f0a7fedab8f8b34aa99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class EventEntity(@Index var eventId: String = "", + @Index var roomId: String = "", + @Index var type: String = "", + var content: String? = null, + var prevContent: String? = null, + var isUseless: Boolean = false, + @Index var stateKey: String? = null, + var originServerTs: Long? = null, + @Index var sender: String? = null, + var age: Long? = 0, + var unsignedData: String? = null, + var redacts: String? = null, + var decryptionResultJson: String? = null, + var decryptionErrorCode: String? = null, + var decryptionErrorReason: String? = null, + var ageLocalTs: Long? = null +) : RealmObject() { + + private var sendStateStr: String = SendState.UNKNOWN.name + + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + + companion object + + fun setDecryptionResult(result: MXEventDecryptionResult) { + val decryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) + decryptionErrorCode = null + decryptionErrorReason = null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..16ae051952bcd9cf70a89c2719e4fd8c2821c507 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +/** + * This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert + * in EventEntity table. + */ +internal open class EventInsertEntity(var eventId: String = "", + var eventType: String = "" +) : RealmObject() { + + private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name + var insertType: EventInsertType + get() { + return EventInsertType.valueOf(insertTypeStr) + } + set(value) { + insertTypeStr = value.name + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java new file mode 100644 index 0000000000000000000000000000000000000000..41ecad003fe0a6ac6ca26a4571745a11c9150325 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model; + +public enum EventInsertType { + INITIAL_SYNC, + INCREMENTAL_SYNC, + PAGINATION, + LOCAL_ECHO +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7a2f90521602f4e5b5ecdf7d742ab431faf9a85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +/** + * Contain a map between Json filter string and filterId (from Homeserver) + * Currently there is only one object in this table + */ +internal open class FilterEntity( + // The serialized FilterBody + var filterBodyJson: String = "", + // The serialized room event filter for pagination + var roomEventFilterJson: String = "", + // the id server side of the filterBodyJson, can be used instead of filterBodyJson if not blank + var filterId: String = "" + +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..76ddb316781b5b2957e09221bd13489c1a4e7ad1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +/** + * This class is used to store group info (groupId and membership) from the sync response. + * Then GetGroupDataTask is called regularly to fetch group information from the homeserver. + */ +internal open class GroupEntity(@PrimaryKey var groupId: String = "") + : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..00c39d4ee4e7b9dc6815a5a6626a860f4730d31b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class GroupSummaryEntity(@PrimaryKey var groupId: String = "", + var displayName: String = "", + var shortDescription: String = "", + var avatarUrl: String = "", + var roomIds: RealmList<String> = RealmList(), + var userIds: RealmList<String> = RealmList() +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b74f053b6b3931e1fed7737fb4f8db6231225c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import io.realm.RealmObject + +internal open class HomeServerCapabilitiesEntity( + var canChangePassword: Boolean = true, + var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastVersionIdentityServerSupported: Boolean = false, + var defaultIdentityServerUrl: String? = null, + var adminE2EByDefault: Boolean = true, + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2b7e0492d999f72ffbd4b4ec9fc34d6091c33d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class IgnoredUserEntity(var userId: String = "") : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..267675ef8a204f9aa1a2eaaf54c166fc0dcc862b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of a poll + */ +internal open class PollResponseAggregatedSummaryEntity( + // For now we persist this a JSON for greater flexibility + // #see PollSummaryContent + var aggregatedContent: String? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList<String> = RealmList(), + var sourceLocalEchoEvents: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..2fdcaa250f714a23f031e7fe9ad2fbc14a378e8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt @@ -0,0 +1,29 @@ +/* + * copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * licensed under the apache license, version 2.0 (the "license"); + * you may not use this file except in compliance with the license. + * you may obtain a copy of the license at + * + * http://www.apache.org/licenses/license-2.0 + * + * unless required by applicable law or agreed to in writing, software + * distributed under the license is distributed on an "as is" basis, + * without warranties or conditions of any kind, either express or implied. + * see the license for the specific language governing permissions and + * limitations under the license. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class PushConditionEntity( + var kind: String = "", + var key: String? = null, + var pattern: String? = null, + var iz: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..118d394e06977de5b70a89cbd0b01a3aeebf0e8d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +internal open class PushRuleEntity( + // Required. The actions to perform when this rule is matched. + var actionsStr: String? = null, + // Required. Whether this is a default rule, or has been set explicitly. + var default: Boolean = false, + // Required. Whether the push rule is enabled or not. + var enabled: Boolean = true, + // Required. The ID of this rule. + var ruleId: String = "", + // The conditions that must hold true for an event in order for a rule to be applied to an event + var conditions: RealmList<PushConditionEntity>? = RealmList(), + // The glob-style pattern to match against. Only applicable to content rules. + var pattern: String? = null +) : RealmObject() { + + @LinkingObjects("pushRules") + val parent: RealmResults<PushRulesEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4a7ef5e0fcc9a5eec057ab1b4ee325a04772c67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.pushrules.RuleKind +import io.realm.RealmList +import io.realm.RealmObject + +internal open class PushRulesEntity( + var scope: String = "", + var pushRules: RealmList<PushRuleEntity> = RealmList() +) : RealmObject() { + + private var kindStr: String = RuleKind.CONTENT.name + var kind: RuleKind + get() { + return RuleKind.valueOf(kindStr) + } + set(value) { + kindStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..9fff183b96d953a9e40330822053709f2deb8ee4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class PusherDataEntity( + var url: String? = null, + var format: String? = null +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b299d4f332e9d987216feca08091a2e2bf58419 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.pushers.PusherState +import io.realm.RealmObject + +// TODO +// at java.lang.Thread.run(Thread.java:764) +// Caused by: java.lang.IllegalArgumentException: 'value' is not a valid managed object. +// at io.realm.ProxyState.checkValidObject(ProxyState.java:213) +// at io.realm.im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy +// .realmSet$data(im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.java:413) +// at org.matrix.android.sdk.internal.database.model.PusherEntity.setData(PusherEntity.kt:16) +// at org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70) +// at io.realm.Realm.executeTransaction(Realm.java:1493) +internal open class PusherEntity( + var pushKey: String = "", + var kind: String? = null, + var appId: String = "", + var appDisplayName: String? = null, + var deviceDisplayName: String? = null, + var profileTag: String? = null, + var lang: String? = null, + var data: PusherDataEntity? = null +) : RealmObject() { + private var stateStr: String = PusherState.UNREGISTERED.name + + var state: PusherState + get() { + try { + return PusherState.valueOf(stateStr) + } catch (e: Exception) { + // can this happen? + return PusherState.UNREGISTERED + } + } + set(value) { + stateStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7da933c6e49ddfe9078a69725af3ac3919824ff3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Aggregated Summary of a reaction. + */ +internal open class ReactionAggregatedSummaryEntity( + // The reaction String 😀 + var key: String = "", + // Number of time this reaction was selected + var count: Int = 0, + // Did the current user sent this reaction + var addedByMe: Boolean = false, + // The first time this reaction was added (for ordering purpose) + var firstTimestamp: Long = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList<String> = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..739c0b9e88641c041c41d27cbb4b0854da34653f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ReadMarkerEntity( + @PrimaryKey + var roomId: String = "", + var eventId: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..f1e3bc4e650cfee1b581fffd9a96b3e7ff853ef3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "", + var eventId: String = "", + var roomId: String = "", + var userId: String = "", + var originServerTs: Double = 0.0 +) : RealmObject() { + companion object + + @LinkingObjects("readReceipts") + val summary: RealmResults<ReadReceiptsSummaryEntity>? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..8445abdb4cdd81da5e3ed0bf8bd74e61c253b6e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String = "", + var readReceipts: RealmList<ReadReceiptEntity> = RealmList() +) : RealmObject() { + + @LinkingObjects("readReceipts") + val timelineEvent: RealmResults<TimelineEventEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..327648abbc1e863f8be709aed807150b94db30ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class ReferencesAggregatedSummaryEntity( + var eventId: String = "", + var content: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList<String> = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae1e7865d2faa7f9c9faf035412d49ef37568e4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class RoomEntity(@PrimaryKey var roomId: String = "", + var chunks: RealmList<ChunkEntity> = RealmList(), + var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(), + var areAllMembersLoaded: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..f2ea5a5f1659ca64833b67397ddf47e4d0495232 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + @Index var displayName: String? = null, + var avatarUrl: String? = null, + var reason: String? = null, + var isDirect: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6859f1d3f7465f61f4578a712462e3a036fb595 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.VersioningState +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class RoomSummaryEntity( + @PrimaryKey var roomId: String = "", + var displayName: String? = "", + var avatarUrl: String? = "", + var name: String? = "", + var topic: String? = "", + var latestPreviewableEvent: TimelineEventEntity? = null, + var heroes: RealmList<String> = RealmList(), + var joinedMembersCount: Int? = 0, + var invitedMembersCount: Int? = 0, + var isDirect: Boolean = false, + var directUserId: String? = null, + var otherMemberIds: RealmList<String> = RealmList(), + var notificationCount: Int = 0, + var highlightCount: Int = 0, + var readMarkerId: String? = null, + var hasUnreadMessages: Boolean = false, + var tags: RealmList<RoomTagEntity> = RealmList(), + var userDrafts: UserDraftsEntity? = null, + var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, + var canonicalAlias: String? = null, + var aliases: RealmList<String> = RealmList(), + // this is required for querying + var flatAliases: String = "", + var isEncrypted: Boolean = false, + var encryptionEventTs: Long? = 0, + var roomEncryptionTrustLevelStr: String? = null, + var inviterId: String? = null, + var hasFailedSending: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + private var versioningStateStr: String = VersioningState.NONE.name + var versioningState: VersioningState + get() { + return VersioningState.valueOf(versioningStateStr) + } + set(value) { + versioningStateStr = value.name + } + + var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? + get() { + return roomEncryptionTrustLevelStr?.let { + try { + RoomEncryptionTrustLevel.valueOf(it) + } catch (failure: Throwable) { + null + } + } + } + set(value) { + roomEncryptionTrustLevelStr = value?.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..8fdae3205d942953ff4434cc836ea145e2a937ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class RoomTagEntity( + var tagName: String = "", + var tagOrder: Double? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8fc4547200bcebce261515c0af795a8efbfa888 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ScalarTokenEntity( + @PrimaryKey var serverUrl: String = "", + var token: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea466db3529af62d47db507fc8e323a02f358110 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.annotations.RealmModule + +/** + * Realm module for Session + */ +@RealmModule(library = true, + classes = [ + ChunkEntity::class, + EventEntity::class, + EventInsertEntity::class, + TimelineEventEntity::class, + FilterEntity::class, + GroupEntity::class, + GroupSummaryEntity::class, + ReadReceiptEntity::class, + RoomEntity::class, + RoomSummaryEntity::class, + RoomTagEntity::class, + SyncEntity::class, + UserEntity::class, + IgnoredUserEntity::class, + BreadcrumbsEntity::class, + UserThreePidEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class, + EditAggregatedSummaryEntity::class, + PollResponseAggregatedSummaryEntity::class, + ReferencesAggregatedSummaryEntity::class, + PushRulesEntity::class, + PushRuleEntity::class, + PushConditionEntity::class, + PusherEntity::class, + PusherDataEntity::class, + ReadReceiptsSummaryEntity::class, + ReadMarkerEntity::class, + UserDraftsEntity::class, + DraftEntity::class, + HomeServerCapabilitiesEntity::class, + RoomMemberSummaryEntity::class, + CurrentStateEventEntity::class, + UserAccountDataEntity::class, + ScalarTokenEntity::class, + WellknownIntegrationManagerConfigEntity::class + ]) +internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a1bcbc8d015f6a8b56d17f6846d14441b5625f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class SyncEntity(var nextBatch: String? = null, + @PrimaryKey var id: Long = 0 +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..36f6041fe6ca2a41f6c971ae2f2b6e231c9b692a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Index +import io.realm.annotations.LinkingObjects + +internal open class TimelineEventEntity(var localId: Long = 0, + @Index var eventId: String = "", + @Index var roomId: String = "", + @Index var displayIndex: Int = 0, + var root: EventEntity? = null, + var annotations: EventAnnotationsSummaryEntity? = null, + var senderName: String? = null, + var isUniqueDisplayName: Boolean = false, + var senderAvatar: String? = null, + var senderMembershipEventId: String? = null, + var readReceipts: ReadReceiptsSummaryEntity? = null +) : RealmObject() { + + @LinkingObjects("timelineEvents") + val chunk: RealmResults<ChunkEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..75aacd8dda2b039666e8d14c30a17b8d89f6ebe6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Clients can store custom config data for their account on their HomeServer. + * This account data will be synced between different devices and can persist across installations on a particular device. + * Users may only view the account data for their own account. + * The account_data may be either global or scoped to a particular rooms. + */ +internal open class UserAccountDataEntity( + @Index var type: String? = null, + var contentStr: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..f84a7b930f1ab37471e2f10f27ec597f4b38f6c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +/** + * Create a specific table to be able to do direct query on it and keep the draft ordered + */ +internal open class UserDraftsEntity(var userDrafts: RealmList<DraftEntity> = RealmList() +) : RealmObject() { + + // Link to RoomSummaryEntity + @LinkingObjects("userDrafts") + val roomSummaryEntity: RealmResults<RoomSummaryEntity>? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..e2150103d9286828f4edb50a71b5dffd25b73322 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class UserEntity(@PrimaryKey var userId: String = "", + var displayName: String = "", + var avatarUrl: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c7337f6a423674fb7014538f63e690fdd4db3b7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class UserThreePidEntity( + var medium: String = "", + var address: String = "", + var validatedAt: Long = 0, + var addedAt: Long = 0 +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..fdabed3c23d27458a2aea3af335b3c07e6f7be72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class WellknownIntegrationManagerConfigEntity( + @PrimaryKey var id: Long = 0, + var apiUrl: String = "", + var uiUrl: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt new file mode 100644 index 0000000000000000000000000000000000000000..e711e301884cda17c260e65221414dbf2fcc0b6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? { + return realm.where<BreadcrumbsEntity>().findFirst() +} + +internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity { + return get(realm) ?: realm.createObject() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..79b611115cb745cbdfceebe5551f2009625dab80 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ChunkEntity> { + return realm.where<ChunkEntity>() + .equalTo("${ChunkEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId) +} + +internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity? { + val query = where(realm, roomId) + if (prevToken != null) { + query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken) + } + if (nextToken != null) { + query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken) + } + return query.findFirst() +} + +internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { + return where(realm, roomId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .findFirst() +} + +internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> { + return realm.where<ChunkEntity>() + .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) + .findAll() +} + +internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? { + return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() +} + +internal fun ChunkEntity.Companion.create( + realm: Realm, + prevToken: String?, + nextToken: String? +): ChunkEntity { + return realm.createObject<ChunkEntity>().apply { + this.prevToken = prevToken + this.nextToken = nextToken + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac00f791b82001329f828252401276c1f769f9a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject + +internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> { + return realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .equalTo(CurrentStateEventEntityFields.TYPE, type) +} + +internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String) + : RealmQuery<CurrentStateEventEntity> { + return whereType(realm = realm, roomId = roomId, type = type) + .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) +} + +internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? { + return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst() +} + +internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type) +} + +private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return realm.createObject<CurrentStateEventEntity>().apply { + this.type = type + this.roomId = roomId + this.stateKey = stateKey + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt new file mode 100644 index 0000000000000000000000000000000000000000..9fa710a94b3ead36860fc3ce4d43e3626618190c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> { + val query = realm.where<EventAnnotationsSummaryEntity>() + query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<EventAnnotationsSummaryEntity> { + val query = realm.where<EventAnnotationsSummaryEntity>() + if (roomId != null) { + query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } + // Denormalization + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let { + it.annotations = obj + } + return obj +} +internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId).apply { this.roomId = roomId } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee41729e2a93eb8e9f3baf711439cc6b11edf44b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { + val eventEntity = realm.where<EventEntity>() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .equalTo(EventEntityFields.ROOM_ID, roomId) + .findFirst() + return if (eventEntity == null) { + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply { + this.insertType = insertType + } + realm.insert(insertEntity) + // copy this event entity and return it + realm.copyToRealm(this) + } else { + eventEntity + } +} + +internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> { + return realm.where<EventEntity>() + .equalTo(EventEntityFields.EVENT_ID, eventId) +} + +internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> { + return realm.where<EventEntity>() + .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray()) +} + +internal fun EventEntity.Companion.whereType(realm: Realm, + type: String, + roomId: String? = null +): RealmQuery<EventEntity> { + val query = realm.where<EventEntity>() + if (roomId != null) { + query.equalTo(EventEntityFields.ROOM_ID, roomId) + } + return query.equalTo(EventEntityFields.TYPE, type) +} + +internal fun EventEntity.Companion.whereTypes(realm: Realm, + typeList: List<String> = emptyList(), + roomId: String? = null): RealmQuery<EventEntity> { + val query = realm.where<EventEntity>() + query.`in`(EventEntityFields.TYPE, typeList.toTypedArray()) + if (roomId != null) { + query.equalTo(EventEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? { + return this.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .findFirst() +} + +internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean { + return this.find(eventId) != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..33a7bff6060042a1217da0ebf1d7341afba9d9a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.FilterEntity +import org.matrix.android.sdk.internal.session.filter.FilterFactory +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current filter + */ +internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? { + return realm.where<FilterEntity>().findFirst() +} + +/** + * Get the current filter, create one if it does not exist + */ +internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { + return get(realm) ?: realm.createObject<FilterEntity>() + .apply { + filterBodyJson = FilterFactory.createDefaultFilter().toJSONString() + roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() + filterId = "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..1097cce46370fd160ebc8cf9681b52ec84847af3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupEntityFields +import org.matrix.android.sdk.internal.query.process +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQuery<GroupEntity> { + return realm.where<GroupEntity>() + .equalTo(GroupEntityFields.GROUP_ID, groupId) +} + +internal fun GroupEntity.Companion.where(realm: Realm, memberships: List<Membership>): RealmQuery<GroupEntity> { + return realm.where<GroupEntity>().process(GroupEntityFields.MEMBERSHIP_STR, memberships) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..650558ee2f3cd9f9d80a364e063cee5a17e50cf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery<GroupSummaryEntity> { + val query = realm.where<GroupSummaryEntity>() + if (groupId != null) { + query.equalTo(GroupSummaryEntityFields.GROUP_ID, groupId) + } + return query +} + +internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List<String>): RealmQuery<GroupSummaryEntity> { + return realm.where<GroupSummaryEntity>() + .`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray()) +} + +internal fun GroupSummaryEntity.Companion.getOrCreate(realm: Realm, groupId: String): GroupSummaryEntity { + return where(realm, groupId).findFirst() ?: realm.createObject(groupId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..1ebe276fbe7617d67d25fd1e6cddb23d16569722 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current HomeServerCapabilitiesEntity, return null if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.get(realm: Realm): HomeServerCapabilitiesEntity? { + return realm.where<HomeServerCapabilitiesEntity>().findFirst() +} + +/** + * Get the current HomeServerCapabilitiesEntity, create one if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity { + return get(realm) ?: realm.createObject() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf34bc0cd1fb29f217a43b2feb1b3316588b2231 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.model.PushRuleEntityFields +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.model.PushRulesEntityFields +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.model.PusherEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun PusherEntity.Companion.where(realm: Realm, + pushKey: String? = null): RealmQuery<PusherEntity> { + return realm.where<PusherEntity>() + .apply { + if (pushKey != null) { + equalTo(PusherEntityFields.PUSH_KEY, pushKey) + } + } +} + +internal fun PushRulesEntity.Companion.where(realm: Realm, + scope: String, + kind: RuleKind): RealmQuery<PushRulesEntity> { + return realm.where<PushRulesEntity>() + .equalTo(PushRulesEntityFields.SCOPE, scope) + .equalTo(PushRulesEntityFields.KIND_STR, kind.name) +} + +internal fun PushRuleEntity.Companion.where(realm: Realm, + scope: String, + ruleId: String): RealmQuery<PushRuleEntity> { + return realm.where<PushRuleEntity>() + .equalTo("${PushRuleEntityFields.PARENT}.${PushRulesEntityFields.SCOPE}", scope) + .equalTo(PushRuleEntityFields.RULE_ID, ruleId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..636fc9ac7336fc4aba603fb3f6d7e96fde13acd3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> { + return realm.where<ReadMarkerEntity>() + .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) +} + +internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..8ccc12a514c2c3b942b0307f44ff8f90d98e3676 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import io.realm.Realm +import io.realm.RealmConfiguration + +internal fun isEventRead(realmConfiguration: RealmConfiguration, + userId: String?, + roomId: String?, + eventId: String?): Boolean { + if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + if (LocalEcho.isLocalEchoId(eventId)) { + return true + } + var isEventRead = false + + Realm.getInstance(realmConfiguration).use { realm -> + val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use + val eventToCheck = liveChunk.timelineEvents.find(eventId) + isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { + true + } else { + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: return@use + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck.displayIndex + + eventToCheckIndex <= readReceiptIndex + } + } + + return isEventRead +} + +internal fun isReadMarkerMoreRecent(realmConfiguration: RealmConfiguration, + roomId: String?, + eventId: String?): Boolean { + if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + return Realm.getInstance(realmConfiguration).use { realm -> + val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() + val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull() + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false + val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst() + val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull() + if (eventToCheckChunk == readMarkerChunk) { + val readMarkerIndex = readMarkerEvent?.displayIndex ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + eventToCheckIndex <= readMarkerIndex + } else { + eventToCheckChunk?.isLastForward == false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..1eb438190af2b41e2b365e05312eab0ec86d46b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { + return realm.where<ReadReceiptEntity>() + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) +} + +internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> { + return realm.where<ReadReceiptEntity>() + .equalTo(ReadReceiptEntityFields.USER_ID, userId) +} + +internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { + return ReadReceiptEntity().apply { + this.primaryKey = "${roomId}_$userId" + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = originServerTs + } +} + +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId)) + .apply { + this.roomId = roomId + this.userId = userId + } +} + +private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d384a1de679f5823b96f4fc5eca8fb65d1ddc7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<ReadReceiptsSummaryEntity> { + return realm.where<ReadReceiptsSummaryEntity>() + .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) +} + +internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String): RealmQuery<ReadReceiptsSummaryEntity> { + return realm.where<ReadReceiptsSummaryEntity>() + .equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..60f665d4604ae0a5f3fe3acfde37ba9bb81685ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReferencesAggregatedSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<ReferencesAggregatedSummaryEntity> { + val query = realm.where<ReferencesAggregatedSummaryEntity>() + query.equalTo(ReferencesAggregatedSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, txID: String): ReferencesAggregatedSummaryEntity { + return realm.createObject(ReferencesAggregatedSummaryEntity::class.java).apply { + this.eventId = txID + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..35d21f8f5f9cb1442c3b2eb81d1290ea29d89858 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<RoomEntity> { + return realm.where<RoomEntity>() + .equalTo(RoomEntityFields.ROOM_ID, roomId) +} + +internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery<RoomEntity> { + val query = realm.where<RoomEntity>() + if (membership != null) { + query.equalTo(RoomEntityFields.MEMBERSHIP_STR, membership.name) + } + return query +} + +internal fun RoomEntity.fastContains(eventId: String): Boolean { + return EventEntity.where(realm, eventId = eventId).findFirst() != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae1d42772d87f7e1f2c9beb2ff02848f75990735 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberSummaryEntity> { + val query = realm + .where<RoomMemberSummaryEntity>() + .equalTo(RoomMemberSummaryEntityFields.ROOM_ID, roomId) + + if (userId != null) { + query.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..7eee63c7d5d1360b80bd3a2ee95ae23bcb49fcd1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> { + val query = realm.where<RoomSummaryEntity>() + if (roomId != null) { + query.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? { + val roomSummary = realm.where<RoomSummaryEntity>() + .equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias) + .findFirst() + if (roomSummary != null) { + return roomSummary + } + return realm.where<RoomSummaryEntity>() + .contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias") + .findFirst() +} + +internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) +} + +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() +} + +internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() + .isNotEmpty() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt new file mode 100644 index 0000000000000000000000000000000000000000..24387856b605dba1e9e5d1d52e68d0bb5bd83793 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ScalarTokenEntity.Companion.where(realm: Realm, serverUrl: String): RealmQuery<ScalarTokenEntity> { + return realm + .where<ScalarTokenEntity>() + .equalTo(ScalarTokenEntityFields.SERVER_URL, serverUrl) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..83075a192ca925cb40fefc6fa6231a87e889f9b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import io.realm.kotlin.where + +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> { + return realm.where<TimelineEventEntity>() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) +} + +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventIds: List<String>): RealmQuery<TimelineEventEntity> { + return realm.where<TimelineEventEntity>() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray()) +} + +internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm, + roomId: String): RealmQuery<TimelineEventEntity> { + return realm.where<TimelineEventEntity>() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) +} + +internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> { + return realm.where<TimelineEventEntity>() + .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) + .findAll() +} + +internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, + roomId: String, + includesSending: Boolean, + filterContentRelation: Boolean = false, + filterTypes: List<String> = emptyList()): TimelineEventEntity? { + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null + val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) + val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) + if (filterContentRelation) { + liveEvents + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { + sendingTimelineEvents + } else { + liveEvents + } + return query + ?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + ?.findFirst() +} + +internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> { + return if (filterTypes.isEmpty()) { + this + } else { + this.`in`(TimelineEventEntityFields.ROOT.TYPE, filterTypes.toTypedArray()) + } +} + +internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEventEntity? { + return this.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() +} + +internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Realm, + roomId: String, + sendStates: List<SendState>) + : RealmResults<TimelineEventEntity> { + return whereRoomId(realm, roomId) + .filterSendStates(sendStates) + .findAll() +} + +internal fun RealmQuery<TimelineEventEntity>.filterSendStates(sendStates: List<SendState>): RealmQuery<TimelineEventEntity> { + val sendStatesStr = sendStates.map { it.name }.toTypedArray() + return `in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..068ec0eb8e9d3bda0fad4424332210ca90e1bcd7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +/** + * Query strings used to filter the timeline events regarding the Json raw string of the Event + */ +internal object TimelineEventFilter { + /** + * To apply to Event.content + */ + internal object Content { + internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}""" + } + + /** + * To apply to Event.decryptionResultJson + */ + internal object DecryptedContent { + internal const val URL = """{*"file":*"url":*}""" + } + + /** + * To apply to Event.unsigned + */ + internal object Unsigned { + internal const val REDACTED = """{*"redacted_because":*}""" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c3bc70787a2eb81340ca33488c3e96fd6e4e7d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.UserDraftsEntity +import org.matrix.android.sdk.internal.database.model.UserDraftsEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<UserDraftsEntity> { + val query = realm.where<UserDraftsEntity>() + if (roomId != null) { + query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt new file mode 100644 index 0000000000000000000000000000000000000000..5566028d60956e299b3bca8692a33e1fc93d8df6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.model.UserEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserEntity.Companion.where(realm: Realm, userId: String): RealmQuery<UserEntity> { + return realm + .where<UserEntity>() + .equalTo(UserEntityFields.USER_ID, userId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt new file mode 100644 index 0000000000000000000000000000000000000000..237eae38ec34eb55e20aa8632f8d174e154572ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Authenticated + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class AuthenticatedIdentity + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Unauthenticated + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UnauthenticatedWithCertificate + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UnauthenticatedWithCertificateWithProgress diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt new file mode 100644 index 0000000000000000000000000000000000000000..9442dc48653c2cb80a380601f1fd5d6df7c95b31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class AuthDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class CryptoDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class IdentityDatabase diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d140232dfbf249b5069e0818f26666f075f93d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionFilesDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionDownloadsDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class CacheDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class ExternalFilesDirectory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..816a674d8184f75b11acf06feaae3adb5db6007c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import android.content.res.Resources +import com.squareup.moshi.Moshi +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.AuthModule +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import org.matrix.olm.OlmManager +import java.io.File + +@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class]) +@MatrixScope +internal interface MatrixComponent { + + fun matrixCoroutineDispatchers(): MatrixCoroutineDispatchers + + fun moshi(): Moshi + + @Unauthenticated + fun okHttpClient(): OkHttpClient + + @MockHttpInterceptor + fun testInterceptor(): TestInterceptor? + + fun authenticationService(): AuthenticationService + + fun context(): Context + + fun matrixConfiguration(): MatrixConfiguration + + fun resources(): Resources + + @CacheDirectory + fun cacheDir(): File + + @ExternalFilesDirectory + fun externalFilesDir(): File? + + fun olmManager(): OlmManager + + fun taskExecutor(): TaskExecutor + + fun sessionParamsStore(): SessionParamsStore + + fun backgroundDetectionObserver(): BackgroundDetectionObserver + + fun sessionManager(): SessionManager + + fun inject(matrix: Matrix) + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context, + @BindsInstance matrixConfiguration: MatrixConfiguration): MatrixComponent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..be3175c22fac9a58e17a781fa46417982d8df2f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import android.content.res.Resources +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher +import org.matrix.olm.OlmManager +import java.io.File +import java.util.concurrent.Executors + +@Module +internal object MatrixModule { + + @JvmStatic + @Provides + @MatrixScope + fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { + return MatrixCoroutineDispatchers(io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main, + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), + dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + } + + @JvmStatic + @Provides + fun providesResources(context: Context): Resources { + return context.resources + } + + @JvmStatic + @Provides + @CacheDirectory + fun providesCacheDir(context: Context): File { + return context.cacheDir + } + + @JvmStatic + @Provides + @ExternalFilesDirectory + fun providesExternalFilesDir(context: Context): File? { + return context.getExternalFilesDir(null) + } + + @JvmStatic + @Provides + @MatrixScope + fun providesOlmManager(): OlmManager { + return OlmManager() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt new file mode 100644 index 0000000000000000000000000000000000000000..8cfa48f26c261e9a281562aea9ddb497b50d0f88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Scope + +/** + * Use the annotation @MatrixScope to annotate classes we want the SDK to instantiate only once + */ +@Scope +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +internal annotation class MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e16d0b455de5e19896ed4d65eb51568d98f24bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent +import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent +import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.internal.network.parsing.ForceToBooleanJsonAdapter +import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory +import org.matrix.android.sdk.internal.network.parsing.UriMoshiAdapter + +object MoshiProvider { + + private val moshi: Moshi = Moshi.Builder() + .add(UriMoshiAdapter()) + .add(ForceToBooleanJsonAdapter()) + .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) + .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) + .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) + .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) + .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) + .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) + .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) + .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) + .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) + .registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS) + .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_RESPONSE) + ) + .add(SerializeNulls.JSON_ADAPTER_FACTORY) + .build() + + fun providesMoshi(): Moshi { + return moshi + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..71961d02d3191d8c6c1f4cd0da7e9da9e1a2bf5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.network.TimeOutInterceptor +import org.matrix.android.sdk.internal.network.UserAgentInterceptor +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okreplay.OkReplayInterceptor +import java.util.concurrent.TimeUnit + +@Module +internal object NetworkModule { + + @Provides + @JvmStatic + fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logger = FormattedJsonHttpLogger() + val interceptor = HttpLoggingInterceptor(logger) + interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL + return interceptor + } + + @Provides + @JvmStatic + fun providesOkReplayInterceptor(): OkReplayInterceptor { + return OkReplayInterceptor() + } + + @Provides + @JvmStatic + fun providesStethoInterceptor(): StethoInterceptor { + return StethoInterceptor() + } + + @Provides + @JvmStatic + fun providesCurlLoggingInterceptor(): CurlLoggingInterceptor { + return CurlLoggingInterceptor() + } + + @MatrixScope + @Provides + @JvmStatic + @Unauthenticated + fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration, + stethoInterceptor: StethoInterceptor, + timeoutInterceptor: TimeOutInterceptor, + userAgentInterceptor: UserAgentInterceptor, + httpLoggingInterceptor: HttpLoggingInterceptor, + curlLoggingInterceptor: CurlLoggingInterceptor, + okReplayInterceptor: OkReplayInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .addNetworkInterceptor(stethoInterceptor) + .addInterceptor(timeoutInterceptor) + .addInterceptor(userAgentInterceptor) + .addInterceptor(httpLoggingInterceptor) + .apply { + if (BuildConfig.LOG_PRIVATE_DATA) { + addInterceptor(curlLoggingInterceptor) + } + matrixConfiguration.proxy?.let { + proxy(it) + } + } + .addInterceptor(okReplayInterceptor) + .build() + } + + @Provides + @JvmStatic + fun providesMoshi(): Moshi { + return MoshiProvider.providesMoshi() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d74c6055b194ad2b151fbb0de761404a5676adac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor + +@Module +internal object NoOpTestModule { + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt new file mode 100644 index 0000000000000000000000000000000000000000..a66c7ff713699178ad884a2adc747b59c8ec6794 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import androidx.annotation.Nullable +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +@Retention(AnnotationRetention.RUNTIME) +@JsonQualifier +annotation class SerializeNulls { + companion object { + val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory { + @Nullable + override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? { + val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java) + ?: return null + return moshi.nextAdapter<Any>(this, type, nextAnnotations).serializeNulls() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b9ea7a0ad820f196121ab5ac740ffe3b5d4d93b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import com.squareup.inject.assisted.dagger2.AssistedModule +import dagger.Module + +@AssistedModule +@Module(includes = [AssistedInject_SessionAssistedInjectModule::class]) +interface SessionAssistedInjectModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt new file mode 100644 index 0000000000000000000000000000000000000000..10a523bbf7a7df2863849354a7efe0586026ffc2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +/** + * Used to inject the userId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UserId + +/** + * Used to inject the deviceId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class DeviceId + +/** + * Used to inject the md5 of the userId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UserMd5 + +/** + * Used to inject the sessionId, which is defined as md5(userId|deviceId) + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..737b5335d4715a1739c26c531c24be52854d22a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import androidx.work.Constraints +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class WorkManagerProvider @Inject constructor( + context: Context, + @SessionId private val sessionId: String +) { + private val tag = MATRIX_SDK_TAG_PREFIX + sessionId + + val workManager = WorkManager.getInstance(context) + + /** + * Create a OneTimeWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun <reified W : ListenableWorker> matrixOneTimeWorkRequestBuilder() = + OneTimeWorkRequestBuilder<W>() + .addTag(tag) + + /** + * Create a PeriodicWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun <reified W : ListenableWorker> matrixPeriodicWorkRequestBuilder(repeatInterval: Long, + repeatIntervalTimeUnit: TimeUnit) = + PeriodicWorkRequestBuilder<W>(repeatInterval, repeatIntervalTimeUnit) + .addTag(tag) + + /** + * Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions + */ + fun cancelAllWorks() { + workManager.let { + it.cancelAllWorkByTag(tag) + it.pruneWork() + } + } + + companion object { + private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" + + /** + * Default constraints: connected network + */ + val workConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + const val BACKOFF_DELAY = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt new file mode 100644 index 0000000000000000000000000000000000000000..b60d60a61bfdb1f5312852e2d14c71f515e2c20b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.eventbus + +import org.greenrobot.eventbus.Logger +import timber.log.Timber +import java.util.logging.Level + +class EventBusTimberLogger : Logger { + override fun log(level: Level, msg: String) { + Timber.d(msg) + } + + override fun log(level: Level, msg: String, th: Throwable) { + Timber.e(th, msg) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt new file mode 100644 index 0000000000000000000000000000000000000000..64fb72e537e5944ec75c1e74c05856e4165a376b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.extensions + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab45f08e4289b45eb773eb08d5410c61ae911dbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.extensions + +/** + * Convert a signed byte to a int value + */ +fun Byte.toUnsignedInt() = toInt() and 0xff diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..ebe9ab7ecb7680d1c05f67cacdfbd2092d88bf2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.extensions + +import io.realm.RealmObject + +internal fun RealmObject.assertIsManaged() { + check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b812736cbb53b3028230b2db17e19df5e9261f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt @@ -0,0 +1,26 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package org.matrix.android.sdk.internal.extensions + +import org.matrix.android.sdk.api.MatrixCallback + +fun <A> Result<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold( + { callback.onSuccess(it) }, + { callback.onFailure(it) } +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt new file mode 100644 index 0000000000000000000000000000000000000000..1030d6717bbf340fba717ebc74dd7839d1414088 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.extensions + +import arrow.core.Failure +import arrow.core.Success +import arrow.core.Try +import arrow.core.TryOf +import arrow.core.fix +import org.matrix.android.sdk.api.MatrixCallback + +inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix() + .fold( + { + f(it) + Failure(it) + }, + { Success(it) } + ) + +fun <A> Try<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold( + { callback.onFailure(it) }, + { callback.onSuccess(it) }) + +/** + * Same as doOnNext for Observables + */ +inline fun <A> Try<A>.alsoDo(f: (A) -> Unit) = map { + f(it) + it +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..1741ca4845a8476d31a2d8c2917d6c45c6fe00e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.legacy + +import android.content.Context +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.DiscoveryInformation +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.legacy.riot.LoginStorage +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import org.matrix.android.sdk.internal.util.md5 +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import org.matrix.android.sdk.internal.legacy.riot.Fingerprint as LegacyFingerprint +import org.matrix.android.sdk.internal.legacy.riot.HomeServerConnectionConfig as LegacyHomeServerConnectionConfig + +internal class DefaultLegacySessionImporter @Inject constructor( + private val context: Context, + private val sessionParamsStore: SessionParamsStore, + private val realmCryptoStoreMigration: RealmCryptoStoreMigration, + private val realmKeysUtils: RealmKeysUtils +) : LegacySessionImporter { + + private val loginStorage = LoginStorage(context) + + companion object { + // During development, set to false to play several times the migration + private var DELETE_PREVIOUS_DATA = true + } + + override fun process(): Boolean { + Timber.d("Migration: Importing legacy session") + + val list = loginStorage.credentialsList + + Timber.d("Migration: found ${list.size} session(s).") + + val legacyConfig = list.firstOrNull() ?: return false + + runBlocking { + Timber.d("Migration: importing a session") + try { + importCredentials(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing credential") + } + + Timber.d("Migration: importing crypto DB") + try { + importCryptoDb(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing crypto DB") + } + + if (DELETE_PREVIOUS_DATA) { + try { + Timber.d("Migration: clear file system") + clearFileSystem(legacyConfig) + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing filesystem") + } + try { + Timber.d("Migration: clear shared prefs") + clearSharedPrefs() + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing shared prefs") + } + } else { + Timber.d("Migration: clear file system - DEACTIVATED") + Timber.d("Migration: clear shared prefs - DEACTIVATED") + } + } + + // A session has been imported + return true + } + + private suspend fun importCredentials(legacyConfig: LegacyHomeServerConnectionConfig) { + @Suppress("DEPRECATION") + val sessionParams = SessionParams( + credentials = Credentials( + userId = legacyConfig.credentials.userId, + accessToken = legacyConfig.credentials.accessToken, + refreshToken = legacyConfig.credentials.refreshToken, + homeServer = legacyConfig.credentials.homeServer, + deviceId = legacyConfig.credentials.deviceId, + discoveryInformation = legacyConfig.credentials.wellKnown?.let { wellKnown -> + // Note credentials.wellKnown is not serialized in the LoginStorage, so this code is a bit useless... + if (wellKnown.homeServer?.baseURL != null || wellKnown.identityServer?.baseURL != null) { + DiscoveryInformation( + homeServer = wellKnown.homeServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) }, + identityServer = wellKnown.identityServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) } + ) + } else { + null + } + } + ), + homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = legacyConfig.homeserverUri, + identityServerUri = legacyConfig.identityServerUri, + antiVirusServerUri = legacyConfig.antiVirusServerUri, + allowedFingerprints = legacyConfig.allowedFingerprints.map { + Fingerprint( + bytes = it.bytes, + hashType = when (it.type) { + LegacyFingerprint.HashType.SHA1, + null -> Fingerprint.HashType.SHA1 + LegacyFingerprint.HashType.SHA256 -> Fingerprint.HashType.SHA256 + } + ) + }, + shouldPin = legacyConfig.shouldPin(), + tlsVersions = legacyConfig.acceptedTlsVersions, + tlsCipherSuites = legacyConfig.acceptedTlsCipherSuites, + shouldAcceptTlsExtensions = legacyConfig.shouldAcceptTlsExtensions(), + allowHttpExtension = false, // TODO + forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions() + ), + // If token is not valid, this boolean will be updated later + isTokenValid = true + ) + + Timber.d("Migration: save session") + sessionParamsStore.save(sessionParams) + } + + private fun importCryptoDb(legacyConfig: LegacyHomeServerConnectionConfig) { + // Here we migrate the DB, we copy the crypto DB to the location specific to RiotX, and we encrypt it. + val userMd5 = legacyConfig.credentials.userId.md5() + + val sessionId = legacyConfig.credentials.let { (if (it.deviceId.isNullOrBlank()) it.userId else "${it.userId}|${it.deviceId}").md5() } + val newLocation = File(context.filesDir, sessionId) + + val keyAlias = "crypto_module_$userMd5" + + // Ensure newLocation does not exist (can happen in case of partial migration) + newLocation.deleteRecursively() + newLocation.mkdirs() + + Timber.d("Migration: create legacy realm configuration") + + val realmConfiguration = RealmConfiguration.Builder() + .directory(File(context.filesDir, userMd5)) + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(realmCryptoStoreMigration) + .build() + + Timber.d("Migration: copy DB to encrypted DB") + Realm.getInstance(realmConfiguration).use { + // Move the DB to the new location, handled by RiotX + it.writeEncryptedCopyTo(File(newLocation, realmConfiguration.realmFileName), realmKeysUtils.getRealmEncryptionKey(keyAlias)) + } + } + + // Delete all the files created by Riot Android which will not be used anymore by RiotX + private fun clearFileSystem(legacyConfig: LegacyHomeServerConnectionConfig) { + val cryptoFolder = legacyConfig.credentials.userId.md5() + + listOf( + // Where session store was saved (we do not care about migrating that, an initial sync will be performed) + File(context.filesDir, "MXFileStore"), + // Previous (and very old) file crypto store + File(context.filesDir, "MXFileCryptoStore"), + // Draft. They will be lost, this is sad but we assume it + File(context.filesDir, "MXLatestMessagesStore"), + // Media storage + File(context.filesDir, "MXMediaStore"), + File(context.filesDir, "MXMediaStore2"), + File(context.filesDir, "MXMediaStore3"), + // Ext folder + File(context.filesDir, "ext_share"), + // Crypto store + File(context.filesDir, cryptoFolder) + ).forEach { file -> + try { + file.deleteRecursively() + } catch (t: Throwable) { + Timber.e(t, "Migration: unable to delete $file") + } + } + } + + private fun clearSharedPrefs() { + // Shared Pref. Note that we do not delete the default preferences, as it should be nearly the same (TODO check that) + listOf( + "Vector.LoginStorage", + "GcmRegistrationManager", + "IntegrationManager.Storage" + ).forEach { prefName -> + context.getSharedPreferences(prefName, Context.MODE_PRIVATE) + .edit() + .clear() + .apply() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java new file mode 100644 index 0000000000000000000000000000000000000000..59ad3be4c51a4a071176d6f41d0d3455a7636b3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.legacy.riot; + +import android.text.TextUtils; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * The user's credentials. + */ +public class Credentials { + public String userId; + + // This is the server name and not a URI, e.g. "matrix.org". Spec says it's now deprecated + @Deprecated + public String homeServer; + + public String accessToken; + + public String refreshToken; + + public String deviceId; + + // Optional data that may contain info to override home server and/or identity server + public WellKnown wellKnown; + + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("user_id", userId); + json.put("home_server", homeServer); + json.put("access_token", accessToken); + json.put("refresh_token", TextUtils.isEmpty(refreshToken) ? JSONObject.NULL : refreshToken); + json.put("device_id", deviceId); + + return json; + } + + public static Credentials fromJson(JSONObject obj) throws JSONException { + Credentials creds = new Credentials(); + creds.userId = obj.getString("user_id"); + creds.homeServer = obj.getString("home_server"); + creds.accessToken = obj.getString("access_token"); + + if (obj.has("device_id")) { + creds.deviceId = obj.getString("device_id"); + } + + // refresh_token is mandatory + if (obj.has("refresh_token")) { + try { + creds.refreshToken = obj.getString("refresh_token"); + } catch (Exception e) { + creds.refreshToken = null; + } + } else { + throw new RuntimeException("refresh_token is required."); + } + + return creds; + } + + @Override + public String toString() { + return "Credentials{" + + "userId='" + userId + '\'' + + ", homeServer='" + homeServer + '\'' + + ", refreshToken.length='" + (refreshToken != null ? refreshToken.length() : "null") + '\'' + + ", accessToken.length='" + (accessToken != null ? accessToken.length() : "null") + '\'' + + '}'; + } + + @Nullable + public String getUserId() { + return userId; + } + + @Nullable + public String getHomeServer() { + return homeServer; + } + + @Nullable + public String getAccessToken() { + return accessToken; + } + + @Nullable + public String getDeviceId() { + return deviceId; + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java new file mode 100644 index 0000000000000000000000000000000000000000..3975618f39f91b51c79d2fc39afe76070586e557 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents a X509 Certificate fingerprint. + */ +public class Fingerprint { + public enum HashType { + SHA1, + SHA256 + } + + private final HashType mHashType; + private final byte[] mBytes; + + public Fingerprint(HashType hashType, byte[] bytes) { + mHashType = hashType; + mBytes = bytes; + } + + public HashType getType() { + return mHashType; + } + + public byte[] getBytes() { + return mBytes; + } + + public JSONObject toJson() throws JSONException { + JSONObject obj = new JSONObject(); + obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT)); + obj.put("hash_type", mHashType.toString()); + return obj; + } + + public static Fingerprint fromJson(JSONObject obj) throws JSONException { + String hashTypeStr = obj.getString("hash_type"); + byte[] fingerprintBytes = Base64.decode(obj.getString("bytes"), Base64.DEFAULT); + + final HashType hashType; + if ("SHA256".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA256; + } else if ("SHA1".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA1; + } else { + throw new JSONException("Unrecognized hash type: " + hashTypeStr); + } + + return new Fingerprint(hashType, fingerprintBytes); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Fingerprint that = (Fingerprint) o; + + if (!Arrays.equals(mBytes, that.mBytes)) return false; + return mHashType == that.mHashType; + + } + + @Override + public int hashCode() { + int result = mBytes != null ? Arrays.hashCode(mBytes) : 0; + result = 31 * result + (mHashType != null ? mHashType.hashCode() : 0); + return result; + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6732a2cd92434f47dae68082108e3fc9b6b62995 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java @@ -0,0 +1,677 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.CipherSuite; +import okhttp3.TlsVersion; +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents how to connect to a specific Homeserver, may include credentials to use. + */ +public class HomeServerConnectionConfig { + + // the home server URI + private Uri mHomeServerUri; + // the jitsi server URI. Can be null + @Nullable + private Uri mJitsiServerUri; + // the identity server URI. Can be null + @Nullable + private Uri mIdentityServerUri; + // the anti-virus server URI + private Uri mAntiVirusServerUri; + // allowed fingerprints + private List<Fingerprint> mAllowedFingerprints = new ArrayList<>(); + // the credentials + private Credentials mCredentials; + // tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints. + private boolean mPin; + // the accepted TLS versions + private List<TlsVersion> mTlsVersions; + // the accepted TLS cipher suites + private List<CipherSuite> mTlsCipherSuites; + // should accept TLS extensions + private boolean mShouldAcceptTlsExtensions = true; + // Force usage of TLS versions + private boolean mForceUsageTlsVersions; + // the proxy hostname + private String mProxyHostname; + // the proxy port + private int mProxyPort = -1; + + + /** + * Private constructor. Please use the Builder + */ + private HomeServerConnectionConfig() { + // Private constructor + } + + /** + * Update the home server URI. + * + * @param uri the new HS uri + */ + public void setHomeserverUri(Uri uri) { + mHomeServerUri = uri; + } + + /** + * @return the home server uri + */ + public Uri getHomeserverUri() { + return mHomeServerUri; + } + + /** + * @return the jitsi server uri + */ + public Uri getJitsiServerUri() { + return mJitsiServerUri; + } + + /** + * @return the identity server uri, or null if not defined + */ + @Nullable + public Uri getIdentityServerUri() { + return mIdentityServerUri; + } + + /** + * @return the anti-virus server uri + */ + public Uri getAntiVirusServerUri() { + if (null != mAntiVirusServerUri) { + return mAntiVirusServerUri; + } + // Else consider the HS uri by default. + return mHomeServerUri; + } + + /** + * @return the allowed fingerprints. + */ + public List<Fingerprint> getAllowedFingerprints() { + return mAllowedFingerprints; + } + + /** + * @return the credentials + */ + public Credentials getCredentials() { + return mCredentials; + } + + /** + * Update the credentials. + * + * @param credentials the new credentials + */ + public void setCredentials(Credentials credentials) { + mCredentials = credentials; + + // Override home server url and/or identity server url if provided + if (credentials.wellKnown != null) { + if (credentials.wellKnown.homeServer != null) { + String homeServerUrl = credentials.wellKnown.homeServer.baseURL; + + if (!TextUtils.isEmpty(homeServerUrl)) { + // remove trailing "/" + if (homeServerUrl.endsWith("/")) { + homeServerUrl = homeServerUrl.substring(0, homeServerUrl.length() - 1); + } + + Timber.d("Overriding homeserver url to " + homeServerUrl); + mHomeServerUri = Uri.parse(homeServerUrl); + } + } + + if (credentials.wellKnown.identityServer != null) { + String identityServerUrl = credentials.wellKnown.identityServer.baseURL; + + if (!TextUtils.isEmpty(identityServerUrl)) { + // remove trailing "/" + if (identityServerUrl.endsWith("/")) { + identityServerUrl = identityServerUrl.substring(0, identityServerUrl.length() - 1); + } + + Timber.d("Overriding identity server url to " + identityServerUrl); + mIdentityServerUri = Uri.parse(identityServerUrl); + } + } + + if (credentials.wellKnown.jitsiServer != null) { + String jitsiServerUrl = credentials.wellKnown.jitsiServer.preferredDomain; + + if (!TextUtils.isEmpty(jitsiServerUrl)) { + // add trailing "/" + if (!jitsiServerUrl.endsWith("/")) { + jitsiServerUrl =jitsiServerUrl + "/"; + } + + Timber.d("Overriding jitsi server url to " + jitsiServerUrl); + mJitsiServerUri = Uri.parse(jitsiServerUrl); + } + } + } + } + + /** + * @return whether we should reject X509 certs that were issued by trusts CAs and only trust + * certs with matching fingerprints. + */ + public boolean shouldPin() { + return mPin; + } + + /** + * TLS versions accepted for TLS connections with the home server. + */ + @Nullable + public List<TlsVersion> getAcceptedTlsVersions() { + return mTlsVersions; + } + + /** + * TLS cipher suites accepted for TLS connections with the home server. + */ + @Nullable + public List<CipherSuite> getAcceptedTlsCipherSuites() { + return mTlsCipherSuites; + } + + /** + * @return whether we should accept TLS extensions. + */ + public boolean shouldAcceptTlsExtensions() { + return mShouldAcceptTlsExtensions; + } + + /** + * @return true if the usage of TlsVersions has to be forced + */ + public boolean forceUsageOfTlsVersions() { + return mForceUsageTlsVersions; + } + + + /** + * @return proxy config if available + */ + @Nullable + public Proxy getProxyConfig() { + if (mProxyHostname == null || mProxyHostname.length() == 0 || mProxyPort == -1) { + return null; + } + + return new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(mProxyHostname, mProxyPort)); + } + + + @Override + public String toString() { + return "HomeserverConnectionConfig{" + + "mHomeServerUri=" + mHomeServerUri + + ", mJitsiServerUri=" + mJitsiServerUri + + ", mIdentityServerUri=" + mIdentityServerUri + + ", mAntiVirusServerUri=" + mAntiVirusServerUri + + ", mAllowedFingerprints size=" + mAllowedFingerprints.size() + + ", mCredentials=" + mCredentials + + ", mPin=" + mPin + + ", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions + + ", mProxyHostname=" + (null == mProxyHostname ? "" : mProxyHostname) + + ", mProxyPort=" + (-1 == mProxyPort ? "" : mProxyPort) + + ", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) + + ", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) + + '}'; + } + + /** + * Convert the object instance into a JSon object + * + * @return the JSon representation + * @throws JSONException the JSON conversion failure reason + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("home_server_url", mHomeServerUri.toString()); + Uri jitsiServerUri = getJitsiServerUri(); + if (jitsiServerUri != null) { + json.put("jitsi_server_url", jitsiServerUri.toString()); + } + Uri identityServerUri = getIdentityServerUri(); + if (identityServerUri != null) { + json.put("identity_server_url", identityServerUri.toString()); + } + + if (mAntiVirusServerUri != null) { + json.put("antivirus_server_url", mAntiVirusServerUri.toString()); + } + + json.put("pin", mPin); + + if (mCredentials != null) json.put("credentials", mCredentials.toJson()); + if (mAllowedFingerprints != null) { + List<JSONObject> fingerprints = new ArrayList<>(mAllowedFingerprints.size()); + + for (Fingerprint fingerprint : mAllowedFingerprints) { + fingerprints.add(fingerprint.toJson()); + } + + json.put("fingerprints", new JSONArray(fingerprints)); + } + + json.put("tls_extensions", mShouldAcceptTlsExtensions); + + if (mTlsVersions != null) { + List<String> tlsVersions = new ArrayList<>(mTlsVersions.size()); + + for (TlsVersion tlsVersion : mTlsVersions) { + tlsVersions.add(tlsVersion.javaName()); + } + + json.put("tls_versions", new JSONArray(tlsVersions)); + } + + json.put("force_usage_of_tls_versions", mForceUsageTlsVersions); + + if (mTlsCipherSuites != null) { + List<String> tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size()); + + for (CipherSuite tlsCipherSuite : mTlsCipherSuites) { + tlsCipherSuites.add(tlsCipherSuite.javaName()); + } + + json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites)); + } + + if (mProxyPort != -1) { + json.put("proxy_port", mProxyPort); + } + + if (mProxyHostname != null && mProxyHostname.length() > 0) { + json.put("proxy_hostname", mProxyHostname); + } + + return json; + } + + /** + * Create an object instance from the json object. + * + * @param jsonObject the json object + * @return a HomeServerConnectionConfig instance + * @throws JSONException the conversion failure reason + */ + public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException { + JSONObject credentialsObj = jsonObject.optJSONObject("credentials"); + Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null; + + Builder builder = new Builder() + .withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url"))) + .withJitsiServerUri(jsonObject.has("jitsi_server_url") ? Uri.parse(jsonObject.getString("jitsi_server_url")) : null) + .withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null) + .withCredentials(creds) + .withPin(jsonObject.optBoolean("pin", false)); + + JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints"); + if (fingerprintArray != null) { + for (int i = 0; i < fingerprintArray.length(); i++) { + builder.addAllowedFingerPrint(Fingerprint.fromJson(fingerprintArray.getJSONObject(i))); + } + } + + // Set the anti-virus server uri if any + if (jsonObject.has("antivirus_server_url")) { + builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url"))); + } + + builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true)); + + // Set the TLS versions if any + if (jsonObject.has("tls_versions")) { + JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions"); + if (tlsVersionsArray != null) { + for (int i = 0; i < tlsVersionsArray.length(); i++) { + builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i))); + } + } + } + + builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false)); + + // Set the TLS cipher suites if any + if (jsonObject.has("tls_cipher_suites")) { + JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites"); + if (tlsCipherSuitesArray != null) { + for (int i = 0; i < tlsCipherSuitesArray.length(); i++) { + builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i))); + } + } + } + + // Set the proxy options right if any + if (jsonObject.has("proxy_hostname") && jsonObject.has("proxy_port")) { + builder.withProxy(jsonObject.getString("proxy_hostname"), jsonObject.getInt("proxy_port")); + } + + return builder.build(); + } + + /** + * Builder + */ + public static class Builder { + private HomeServerConnectionConfig mHomeServerConnectionConfig; + + /** + * Builder constructor + */ + public Builder() { + mHomeServerConnectionConfig = new HomeServerConnectionConfig(); + } + + /** + * create a Builder from an existing HomeServerConnectionConfig + */ + public Builder(HomeServerConnectionConfig from) { + try { + mHomeServerConnectionConfig = HomeServerConnectionConfig.fromJson(from.toJson()); + } catch (JSONException e) { + // Should not happen + throw new RuntimeException("Unable to create a HomeServerConnectionConfig", e); + } + } + + /** + * @param homeServerUri The URI to use to connect to the homeserver. Cannot be null + * @return this builder + */ + public Builder withHomeServerUri(final Uri homeServerUri) { + if (homeServerUri == null || (!"http".equals(homeServerUri.getScheme()) && !"https".equals(homeServerUri.getScheme()))) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + + // remove trailing / + if (homeServerUri.toString().endsWith("/")) { + try { + String url = homeServerUri.toString(); + mHomeServerConnectionConfig.mHomeServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + } else { + mHomeServerConnectionConfig.mHomeServerUri = homeServerUri; + } + + return this; + } + + /** + * @param jitsiServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withJitsiServerUri(@Nullable final Uri jitsiServerUri) { + if (jitsiServerUri != null + && !jitsiServerUri.toString().isEmpty() + && !"http".equals(jitsiServerUri.getScheme()) + && !"https".equals(jitsiServerUri.getScheme())) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + + // add trailing / + if ((null != jitsiServerUri) && !jitsiServerUri.toString().endsWith("/")) { + try { + String url = jitsiServerUri.toString(); + mHomeServerConnectionConfig.mJitsiServerUri = Uri.parse(url + "/"); + } catch (Exception e) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + } else { + if (jitsiServerUri != null && jitsiServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mJitsiServerUri = null; + } else { + mHomeServerConnectionConfig.mJitsiServerUri = jitsiServerUri; + } + } + + return this; + } + + /** + * @param identityServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) { + if (identityServerUri != null + && !identityServerUri.toString().isEmpty() + && !"http".equals(identityServerUri.getScheme()) + && !"https".equals(identityServerUri.getScheme())) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + + // remove trailing / + if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) { + try { + String url = identityServerUri.toString(); + mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + } else { + if (identityServerUri != null && identityServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mIdentityServerUri = null; + } else { + mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri; + } + } + + return this; + } + + /** + * @param credentials The credentials to use, if needed. Can be null. + * @return this builder + */ + public Builder withCredentials(@Nullable Credentials credentials) { + mHomeServerConnectionConfig.mCredentials = credentials; + return this; + } + + /** + * @param allowedFingerprint If using SSL, allow server certs that match this fingerprint. + * @return this builder + */ + public Builder addAllowedFingerPrint(@Nullable Fingerprint allowedFingerprint) { + if (allowedFingerprint != null) { + mHomeServerConnectionConfig.mAllowedFingerprints.add(allowedFingerprint); + } + + return this; + } + + /** + * @param pin If true only allow certs matching given fingerprints, otherwise fallback to + * standard X509 checks. + * @return this builder + */ + public Builder withPin(boolean pin) { + mHomeServerConnectionConfig.mPin = pin; + + return this; + } + + /** + * @param shouldAcceptTlsExtension + * @return this builder + */ + public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) { + mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension; + + return this; + } + + /** + * Add an accepted TLS version for TLS connections with the home server. + * + * @param tlsVersion the tls version to add to the set of TLS versions accepted. + * @return this builder + */ + public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) { + if (mHomeServerConnectionConfig.mTlsVersions == null) { + mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion); + + return this; + } + + /** + * Force the usage of TlsVersion. This can be usefull for device on Android version < 20 + * + * @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)} + * @return this builder + */ + public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) { + mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions; + + return this; + } + + /** + * Add a TLS cipher suite to the list of accepted TLS connections with the home server. + * + * @param tlsCipherSuite the tls cipher suite to add. + * @return this builder + */ + public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) { + if (mHomeServerConnectionConfig.mTlsCipherSuites == null) { + mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite); + + return this; + } + + /** + * Update the anti-virus server URI. + * + * @param antivirusServerUri the new anti-virus uri. Can be null + * @return this builder + */ + public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) { + if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) { + throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri); + } + + mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri; + + return this; + } + + /** + * Convenient method to limit the TLS versions and cipher suites for this Builder + * Ref: + * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://developer.android.com/reference/javax/net/ssl/SSLEngine + * + * @param tlsLimitations true to use Tls limitations + * @param enableCompatibilityMode set to true for Android < 20 + * @return this builder + */ + public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) { + if (tlsLimitations) { + withShouldAcceptTlsExtensions(false); + + // Tls versions + addAcceptedTlsVersion(TlsVersion.TLS_1_2); + addAcceptedTlsVersion(TlsVersion.TLS_1_3); + + forceUsageOfTlsVersions(enableCompatibilityMode); + + // Cipher suites + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256); + + if (enableCompatibilityMode) { + // Adopt some preceding cipher suites for Android < 20 to be able to negotiate + // a TLS session. + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); + } + } + + return this; + } + + /** + * @param proxyHostname Proxy Hostname + * @param proxyPort Proxy Port + * @return this builder + */ + public Builder withProxy(@Nullable String proxyHostname, int proxyPort) { + mHomeServerConnectionConfig.mProxyHostname = proxyHostname; + mHomeServerConnectionConfig.mProxyPort = proxyPort; + return this; + } + + /** + * @return the {@link HomeServerConnectionConfig} + */ + public HomeServerConnectionConfig build() { + // Check mandatory parameters + if (mHomeServerConnectionConfig.mHomeServerUri == null) { + throw new RuntimeException("Home server URI not set"); + } + + return mHomeServerConnectionConfig; + } + + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java new file mode 100755 index 0000000000000000000000000000000000000000..672053d4ccea74c71195519385ae93d6a4760dba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Stores login credentials in SharedPreferences. + */ +public class LoginStorage { + private static final String PREFS_LOGIN = "Vector.LoginStorage"; + + // multi accounts + home server config + private static final String PREFS_KEY_CONNECTION_CONFIGS = "PREFS_KEY_CONNECTION_CONFIGS"; + + private final Context mContext; + + public LoginStorage(Context appContext) { + mContext = appContext.getApplicationContext(); + + } + + /** + * @return the list of home server configurations. + */ + public List<HomeServerConnectionConfig> getCredentialsList() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + + String connectionConfigsString = prefs.getString(PREFS_KEY_CONNECTION_CONFIGS, null); + + Timber.d("Got connection json: "); + + if (connectionConfigsString == null) { + return new ArrayList<>(); + } + + try { + JSONArray connectionConfigsStrings = new JSONArray(connectionConfigsString); + + List<HomeServerConnectionConfig> configList = new ArrayList<>( + connectionConfigsStrings.length() + ); + + for (int i = 0; i < connectionConfigsStrings.length(); i++) { + configList.add( + HomeServerConnectionConfig.fromJson(connectionConfigsStrings.getJSONObject(i)) + ); + } + + return configList; + } catch (JSONException e) { + Timber.e(e, "Failed to deserialize accounts"); + throw new RuntimeException("Failed to deserialize accounts"); + } + } + + /** + * Add a credentials to the credentials list + * + * @param config the home server config to add. + */ + public void addCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List<HomeServerConnectionConfig> configs = getCredentialsList(); + + configs.add(config); + + List<JSONObject> serialized = new ArrayList<>(configs.size()); + + try { + for (HomeServerConnectionConfig c : configs) { + serialized.add(c.toJson()); + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Remove the credentials from credentials list + * + * @param config the credentials to remove + */ + public void removeCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + Timber.d("Removing account: " + config.getCredentials().userId); + + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List<HomeServerConnectionConfig> configs = getCredentialsList(); + List<JSONObject> serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Replace the credential from credentials list, based on credentials.userId. + * If it does not match an existing credential it does *not* insert the new credentials. + * + * @param config the credentials to insert + */ + public void replaceCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List<HomeServerConnectionConfig> configs = getCredentialsList(); + List<JSONObject> serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + serialized.add(config.toJson()); + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Clear the stored values + */ + public void clear() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(PREFS_KEY_CONNECTION_CONFIGS); + //Need to commit now because called before forcing an app restart + editor.commit(); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt new file mode 100644 index 0000000000000000000000000000000000000000..234cd726897bcf84ff560c3271bafbfd527950da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * <pre> + * { + * "m.homeserver": { + * "base_url": "https://matrix.org" + * }, + * "m.identity_server": { + * "base_url": "https://vector.im" + * } + * "m.integrations": { + * "managers": [ + * { + * "api_url": "https://integrations.example.org", + * "ui_url": "https://integrations.example.org/ui" + * }, + * { + * "api_url": "https://bots.example.org" + * } + * ] + * } + * "im.vector.riot.jitsi": { + * "preferredDomain": "https://jitsi.riot.im/" + * } + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +class WellKnown { + + @JvmField + @Json(name = "m.homeserver") + var homeServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.identity_server") + var identityServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.integrations") + var integrations: Map<String, *>? = null + + /** + * Returns the list of integration managers proposed + */ + fun getIntegrationManagers(): List<WellKnownManagerConfig> { + val managers = ArrayList<WellKnownManagerConfig>() + integrations?.get("managers")?.let { + (it as? ArrayList<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + managers.add(WellKnownManagerConfig( + apiUrl = apiUrl, + uiUrl = uiUrl + )) + } + } + } + } + } + return managers + } + + @JvmField + @Json(name = "im.vector.riot.jitsi") + var jitsiServer: WellKnownPreferredConfig? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9efccce3c78d8b16c257fa6a7ddc6a2affb1275 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * <pre> + * { + * "base_url": "https://vector.im" + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +class WellKnownBaseConfig { + + @JvmField + @Json(name = "base_url") + var baseURL: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..f93d90af26b5267e937917ba11aacbdce3dc0c4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.legacy.riot + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +data class WellKnownManagerConfig( + val apiUrl : String, + val uiUrl: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a918d6f0da8b63f35f3f9d2ae43774b8a5cab13 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * <pre> + * { + * "preferredDomain": "https://jitsi.riot.im/" + * } + * </pre> + */ +@JsonClass(generateAdapter = true) +class WellKnownPreferredConfig { + + @JvmField + @Json(name = "preferredDomain") + var preferredDomain: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..6f5b07b229ec19f11bd5da1f3a5306dc9d38d9b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import okhttp3.Interceptor +import okhttp3.Response + +internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + // Add the access token to all requests if it is set + accessTokenProvider.getToken()?.let { token -> + val newRequestBuilder = request.newBuilder() + newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token") + request = newRequestBuilder.build() + } + + return chain.proceed(request) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt new file mode 100644 index 0000000000000000000000000000000000000000..f15b0353e293b489798db9ba3f0d8a651bf52335 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +object HttpHeaders { + + const val Authorization = "Authorization" + const val UserAgent = "User-Agent" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..caf5090ad6e0fe198dd30b08a71d59305d8ad775 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import android.annotation.TargetApi +import android.content.Context +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import androidx.core.content.getSystemService +import timber.log.Timber +import javax.inject.Inject + +internal interface NetworkCallbackStrategy { + fun register(hasChanged: () -> Unit) + fun unregister() +} + +internal class FallbackNetworkCallbackStrategy @Inject constructor(private val context: Context, + private val networkInfoReceiver: NetworkInfoReceiver) : NetworkCallbackStrategy { + + @Suppress("DEPRECATION") + val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + + override fun register(hasChanged: () -> Unit) { + networkInfoReceiver.isConnectedCallback = { + hasChanged() + } + context.registerReceiver(networkInfoReceiver, filter) + } + + override fun unregister() { + networkInfoReceiver.isConnectedCallback = null + context.unregisterReceiver(networkInfoReceiver) + } +} + +@TargetApi(Build.VERSION_CODES.N) +internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy { + + private var hasChangedCallback: (() -> Unit)? = null + private val conn = context.getSystemService<ConnectivityManager>()!! + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + + override fun onLost(network: Network) { + hasChangedCallback?.invoke() + } + + override fun onAvailable(network: Network) { + hasChangedCallback?.invoke() + } + } + + override fun register(hasChanged: () -> Unit) { + hasChangedCallback = hasChanged + conn.registerDefaultNetworkCallback(networkCallback) + } + + override fun unregister() { + // It can crash after an application update, if not registered + val doUnregister = hasChangedCallback != null + hasChangedCallback = null + if (doUnregister) { + // Add a try catch for safety + try { + conn.unregisterNetworkCallback(networkCallback) + } catch (t: Throwable) { + Timber.e(t, "Unable to unregister network callback") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f3e77d80007e65b213e14b0314538c897692988 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.homeserver.HomeServerPinger +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import kotlinx.coroutines.runBlocking +import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +interface NetworkConnectivityChecker { + /** + * Returns true when internet is available + */ + @WorkerThread + fun hasInternetAccess(forcePing: Boolean): Boolean + + fun register(listener: Listener) + fun unregister(listener: Listener) + + interface Listener { + fun onConnectivityChanged() + } +} + +@SessionScope +internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger, + private val backgroundDetectionObserver: BackgroundDetectionObserver, + private val networkCallbackStrategy: NetworkCallbackStrategy) + : NetworkConnectivityChecker { + + private val hasInternetAccess = AtomicBoolean(true) + private val listeners = Collections.synchronizedSet(LinkedHashSet<NetworkConnectivityChecker.Listener>()) + private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener { + override fun onMoveToForeground() { + bind() + } + + override fun onMoveToBackground() { + unbind() + } + } + + /** + * Returns true when internet is available + */ + @WorkerThread + override fun hasInternetAccess(forcePing: Boolean): Boolean { + return if (forcePing) { + runBlocking { + homeServerPinger.canReachHomeServer() + } + } else { + hasInternetAccess.get() + } + } + + override fun register(listener: NetworkConnectivityChecker.Listener) { + if (listeners.isEmpty()) { + if (backgroundDetectionObserver.isInBackground) { + unbind() + } else { + bind() + } + backgroundDetectionObserver.register(backgroundDetectionObserverListener) + } + listeners.add(listener) + } + + override fun unregister(listener: NetworkConnectivityChecker.Listener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + backgroundDetectionObserver.unregister(backgroundDetectionObserverListener) + } + } + + private fun bind() { + networkCallbackStrategy.register { + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnectivityChanged() + } + } + homeServerPinger.canReachHomeServer { + hasInternetAccess.set(it) + } + } + + private fun unbind() { + networkCallbackStrategy.unregister() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb8ea2dc68e55a6105a8da61bcbd950ec2d7a990 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +internal object NetworkConstants { + + private const val URI_API_PREFIX_PATH = "_matrix/client" + const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" + const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" + const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" + + // Media + private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" + const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" + + const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..85a69c853e94a0c79c2e04dc7c6dd684a6ed5fb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +// This BroadcastReceiver is used only if the build code is below 24. +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.network + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.core.content.getSystemService +import javax.inject.Inject + +internal class NetworkInfoReceiver @Inject constructor() : BroadcastReceiver() { + + var isConnectedCallback: ((Boolean) -> Unit)? = null + + override fun onReceive(context: Context, intent: Intent) { + val conn = context.getSystemService<ConnectivityManager>()!! + val networkInfo: NetworkInfo? = conn.activeNetworkInfo + isConnectedCallback?.invoke(networkInfo?.isConnected ?: false) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ce260e54e9b07e2fef05e3026c586aece1bbe5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.Buffer +import okio.BufferedSink +import okio.ForwardingSink +import okio.Sink +import okio.buffer +import java.io.IOException + +internal class ProgressRequestBody(private val delegate: RequestBody, + private val listener: Listener) : RequestBody() { + + private lateinit var countingSink: CountingSink + + override fun contentType(): MediaType? { + return delegate.contentType() + } + + override fun contentLength(): Long { + try { + return delegate.contentLength() + } catch (e: IOException) { + e.printStackTrace() + } + + return -1 + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + countingSink = CountingSink(sink) + val bufferedSink = countingSink.buffer() + delegate.writeTo(bufferedSink) + bufferedSink.flush() + } + + private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) { + + private var bytesWritten: Long = 0 + + @Throws(IOException::class) + override fun write(source: Buffer, byteCount: Long) { + super.write(source, byteCount) + bytesWritten += byteCount + listener.onProgress(bytesWritten, contentLength()) + } + } + + interface Listener { + fun onProgress(current: Long, total: Long) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt new file mode 100644 index 0000000000000000000000000000000000000000..52556e4c2dbf8b2f940268b30036fa14222e16bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.internal.network.ssl.CertUtil +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import org.greenrobot.eventbus.EventBus +import retrofit2.Call +import retrofit2.awaitResponse +import java.io.IOException + +internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?, + block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute() + +internal class Request<DATA : Any>(private val eventBus: EventBus?) { + + var isRetryable = false + var initialDelay: Long = 100L + var maxDelay: Long = 10_000L + var maxRetryCount = Int.MAX_VALUE + private var currentRetryCount = 0 + private var currentDelay = initialDelay + lateinit var apiCall: Call<DATA> + + suspend fun execute(): DATA { + return try { + val response = apiCall.clone().awaitResponse() + if (response.isSuccessful) { + response.body() + ?: throw IllegalStateException("The request returned a null body") + } else { + throw response.toFailure(eventBus) + } + } catch (exception: Throwable) { + // Check if this is a certificateException + CertUtil.getCertificateException(exception) + // TODO Support certificate error once logged + // ?.also { unrecognizedCertificateException -> + // // Send the error to the bus, for a global management + // eventBus?.post(GlobalError.CertificateError(unrecognizedCertificateException)) + // } + ?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException } + + if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { + delay(currentDelay) + currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) + return execute() + } else { + throw when (exception) { + is IOException -> Failure.NetworkConnection(exception) + is Failure.ServerError, + is Failure.OtherServerError -> exception + is CancellationException -> Failure.Cancelled(exception) + else -> Failure.Unknown(exception) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7eacfb4f513c06d0f1c56ea2fc5cba3afffe561 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -0,0 +1,102 @@ +/* + * + * * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.network + +import com.squareup.moshi.JsonEncodingException +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.di.MoshiProvider +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus +import retrofit2.Response +import timber.log.Timber +import java.io.IOException +import java.net.HttpURLConnection +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + cancel() + } + + enqueue(object : okhttp3.Callback { + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + continuation.resume(response) + } + + override fun onFailure(call: okhttp3.Call, e: IOException) { + continuation.resumeWithException(e) + } + }) + } +} + +/** + * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun <T> Response<T>.toFailure(eventBus: EventBus?): Failure { + return toFailure(errorBody(), code(), eventBus) +} + +/** + * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun okhttp3.Response.toFailure(eventBus: EventBus?): Failure { + return toFailure(body, code, eventBus) +} + +private fun toFailure(errorBody: ResponseBody?, httpCode: Int, eventBus: EventBus?): Failure { + if (errorBody == null) { + return Failure.Unknown(RuntimeException("errorBody should not be null")) + } + + val errorBodyStr = errorBody.string() + + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + + try { + val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) + + if (matrixError != null) { + if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { + // Also send this error to the bus, for a global management + eventBus?.post(GlobalError.ConsentNotGivenError(matrixError.consentUri)) + } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { + // Also send this error to the bus, for a global management + eventBus?.post(GlobalError.InvalidToken(matrixError.isSoftLogout)) + } + + return Failure.ServerError(matrixError, httpCode) + } + } catch (ex: Exception) { + // This is not a MatrixError + Timber.w("The error returned by the server is not a MatrixError") + } catch (ex: JsonEncodingException) { + // This is not a MatrixError, HTML code? + Timber.w("The error returned by the server is not a MatrixError, probably HTML string") + } + + return Failure.OtherServerError(errorBodyStr, httpCode) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..368611dd7d09d36d673fda7e9f96020735ffaab0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import com.squareup.moshi.Moshi +import dagger.Lazy +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import javax.inject.Inject + +internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { + + /** + * Use only for authentication service + */ + fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .client(okHttpClient) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } + + fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .callFactory(object : Call.Factory { + override fun newCall(request: Request): Call { + return okHttpClient.get().newCall(request) + } + }) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..4fb8f513d07afdacf7c3e64e7f54573a4d40a571 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Get the specific headers to apply specific timeout + * Inspired from https://github.com/square/retrofit/issues/2561 + */ +internal class TimeOutInterceptor @Inject constructor() : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + val connectTimeout = request.header(CONNECT_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.connectTimeoutMillis() + val readTimeout = request.header(READ_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.readTimeoutMillis() + val writeTimeout = request.header(WRITE_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.writeTimeoutMillis() + + val newRequestBuilder = request.newBuilder() + .removeHeader(CONNECT_TIMEOUT) + .removeHeader(READ_TIMEOUT) + .removeHeader(WRITE_TIMEOUT) + + request = newRequestBuilder.build() + + return chain + .withConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .withReadTimeout(readTimeout, TimeUnit.MILLISECONDS) + .withWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .proceed(request) + } + + companion object { + // Custom header name + const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT" + const val READ_TIMEOUT = "READ_TIMEOUT" + const val WRITE_TIMEOUT = "WRITE_TIMEOUT" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..dfcf041261ff61be731a64af66a518e9443adf6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.ResponseBody +import retrofit2.Converter +import retrofit2.Retrofit +import java.lang.reflect.Type + +object UnitConverterFactory : Converter.Factory() { + override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, + retrofit: Retrofit): Converter<ResponseBody, *>? { + return if (type == Unit::class.java) UnitConverter else null + } + + private object UnitConverter : Converter<ResponseBody, Unit> { + override fun convert(value: ResponseBody) { + value.close() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a5451bddee78e395281bbc0ee2ad19942d5fd11 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import android.content.Context +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +@MatrixScope +internal class UserAgentHolder @Inject constructor(private val context: Context, + matrixConfiguration: MatrixConfiguration) { + + var userAgent: String = "" + private set + + init { + setApplicationFlavor(matrixConfiguration.applicationFlavor) + } + + /** + * Create an user agent with the application version. + * Ex: RiotX/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSDK_X 1.0) + * + * @param flavorDescription the flavor description + */ + private fun setApplicationFlavor(flavorDescription: String) { + var appName = "" + var appVersion = "" + + try { + val appPackageName = context.applicationContext.packageName + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(appPackageName, 0) + appName = pm.getApplicationLabel(appInfo).toString() + + val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) + appVersion = pkgInfo.versionName ?: "" + + // Use appPackageName instead of appName if appName contains any non-ASCII character + if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { + appName = appPackageName + } + } catch (e: Exception) { + Timber.e(e, "## initUserAgent() : failed") + } + + val systemUserAgent = System.getProperty("http.agent") + + // cannot retrieve the application version + if (appName.isEmpty() || appVersion.isEmpty()) { + if (null == systemUserAgent) { + userAgent = "Java" + System.getProperty("java.version") + } + return + } + + // if there is no user agent or cannot parse it + if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { + userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + + "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")") + } else { + // update + userAgent = appName + "/" + appVersion + " " + + systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + + "; Flavour " + flavorDescription + + "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..109975111260d1d21dca87cfc574d751597de966 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +internal class UserAgentInterceptor @Inject constructor(private val userAgentHolder: UserAgentHolder) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val newRequestBuilder = request.newBuilder() + // Add the user agent to all requests if it is set + userAgentHolder.userAgent + .takeIf { it.isNotBlank() } + ?.let { + newRequestBuilder.header(HttpHeaders.UserAgent, it) + } + request = newRequestBuilder.build() + return chain.proceed(request) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..94d1d958578c13e5e4e18106b10cc358fb642314 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.httpclient + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.network.AccessTokenInterceptor +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.ssl.CertUtil +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import okhttp3.OkHttpClient +import timber.log.Timber + +internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + + return this +} + +internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient.Builder { + try { + val pair = CertUtil.newPinnedSSLSocketFactory(homeServerConnectionConfig) + sslSocketFactory(pair.sslSocketFactory, pair.x509TrustManager) + hostnameVerifier(CertUtil.newHostnameVerifier(homeServerConnectionConfig)) + connectionSpecs(CertUtil.newConnectionSpecs(homeServerConnectionConfig)) + } catch (e: Exception) { + Timber.e(e, "addSocketFactory failed") + } + + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt new file mode 100644 index 0000000000000000000000000000000000000000..42a63e0c56a76e1061c5ae1d797d29368b4b4a14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.parsing + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.JsonReader +import com.squareup.moshi.ToJson +import timber.log.Timber + +@JsonQualifier +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) +annotation class ForceToBoolean + +internal class ForceToBooleanJsonAdapter { + @ToJson + fun toJson(@ForceToBoolean b: Boolean): Boolean { + return b + } + + @FromJson + @ForceToBoolean + fun fromJson(reader: JsonReader): Boolean { + return when (val token = reader.peek()) { + JsonReader.Token.NUMBER -> reader.nextInt() != 0 + JsonReader.Token.BOOLEAN -> reader.nextBoolean() + else -> { + Timber.e("Expecting a boolean or a int but get: $token") + reader.skipValue() + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..a49660523efccaa361ee297125be568c9a15e63c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.parsing; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.CheckReturnValue; + +/** + * A JsonAdapter factory for polymorphic types. This is useful when the type is not known before + * decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a + * key whose value is a label that determines the type to which to map the JSON object. + */ +public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory { + final Class<T> baseType; + final String labelKey; + final Class fallbackType; + final Map<String, Type> labelToType = new LinkedHashMap<>(); + + /** + * @param baseType The base type for which this factory will create adapters. Cannot be Object. + * @param labelKey The key in the JSON object whose value determines the type to which to map the + * JSON object. + */ + @CheckReturnValue + public static <T> RuntimeJsonAdapterFactory<T> of(Class<T> baseType, String labelKey, Class<? extends T> fallbackType) { + if (baseType == null) throw new NullPointerException("baseType == null"); + if (labelKey == null) throw new NullPointerException("labelKey == null"); + if (baseType == Object.class) { + throw new IllegalArgumentException( + "The base type must not be Object. Consider using a marker interface."); + } + return new RuntimeJsonAdapterFactory<>(baseType, labelKey, fallbackType); + } + + RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey, Class fallbackType) { + this.baseType = baseType; + this.labelKey = labelKey; + this.fallbackType = fallbackType; + } + + /** + * Register the subtype that can be created based on the label. When an unknown type is found + * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label + * is found during decoding a {@linkplain JsonDataException} will be thrown. + */ + public RuntimeJsonAdapterFactory<T> registerSubtype(Class<? extends T> subtype, String label) { + if (subtype == null) throw new NullPointerException("subtype == null"); + if (label == null) throw new NullPointerException("label == null"); + if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) { + throw new IllegalArgumentException("Subtypes and labels must be unique."); + } + labelToType.put(label, subtype); + return this; + } + + @Override + public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) { + if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { + return null; + } + int size = labelToType.size(); + Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size); + Map<Type, String> typeToLabel = new LinkedHashMap<>(size); + for (Map.Entry<String, Type> entry : labelToType.entrySet()) { + String label = entry.getKey(); + Type typeValue = entry.getValue(); + typeToLabel.put(typeValue, label); + labelToAdapter.put(label, moshi.adapter(typeValue)); + } + + final JsonAdapter<Object> fallbackAdapter = moshi.adapter(fallbackType); + JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class); + + return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel, + objectJsonAdapter, fallbackAdapter).nullSafe(); + } + + static final class RuntimeJsonAdapter extends JsonAdapter<Object> { + final String labelKey; + final Map<String, JsonAdapter<Object>> labelToAdapter; + final Map<Type, String> typeToLabel; + final JsonAdapter<Object> objectJsonAdapter; + final JsonAdapter<Object> fallbackAdapter; + + RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter, + Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter, + JsonAdapter<Object> fallbackAdapter) { + this.labelKey = labelKey; + this.labelToAdapter = labelToAdapter; + this.typeToLabel = typeToLabel; + this.objectJsonAdapter = objectJsonAdapter; + this.fallbackAdapter = fallbackAdapter; + } + + @Override + public Object fromJson(JsonReader reader) throws IOException { + JsonReader.Token peekedToken = reader.peek(); + if (peekedToken != JsonReader.Token.BEGIN_OBJECT) { + throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken + + " at path " + reader.getPath()); + } + Object jsonValue = reader.readJsonValue(); + Map<String, Object> jsonObject = (Map<String, Object>) jsonValue; + Object label = jsonObject.get(labelKey); + if (!(label instanceof String)) { + return null; + } + JsonAdapter<Object> adapter = labelToAdapter.get(label); + if (adapter == null) { + return fallbackAdapter.fromJsonValue(jsonValue); + } + return adapter.fromJsonValue(jsonValue); + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + Class<?> type = value.getClass(); + String label = typeToLabel.get(type); + if (label == null) { + throw new IllegalArgumentException("Expected one of " + + typeToLabel.keySet() + + " but found " + + value + + ", a " + + value.getClass() + + ". Register this subtype."); + } + JsonAdapter<Object> adapter = labelToAdapter.get(label); + Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value); + + Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size()); + valueWithLabel.put(labelKey, label); + valueWithLabel.putAll(jsonValue); + objectJsonAdapter.toJson(writer, valueWithLabel); + } + + @Override + public String toString() { + return "RuntimeJsonAdapter(" + labelKey + ")"; + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..f45c89304bc97d680c9005f51fe0c2909b23f0e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.parsing + +import android.net.Uri +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson + +internal class UriMoshiAdapter { + + @ToJson + fun toJson(uri: Uri): String { + return uri.toString() + } + + @FromJson + fun fromJson(uriString: String): Uri { + return Uri.parse(uriString) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..40a8e29829a24bd9938be598055f3693118bf334 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import okhttp3.ConnectionSpec +import okhttp3.internal.tls.OkHostnameVerifier +import timber.log.Timber +import java.security.KeyStore +import java.security.MessageDigest +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +/** + * Various utility classes for dealing with X509Certificates + */ +internal object CertUtil { + + // Set to false to do some test + private const val USE_DEFAULT_HOSTNAME_VERIFIER = true + + private val hexArray = "0123456789ABCDEF".toCharArray() + + /** + * Generates the SHA-256 fingerprint of the given certificate + * + * @param cert the certificate. + * @return the finger print + * @throws CertificateException the certificate exception + */ + @Throws(CertificateException::class) + fun generateSha256Fingerprint(cert: X509Certificate): ByteArray { + return generateFingerprint(cert, "SHA-256") + } + + /** + * Generates the SHA-1 fingerprint of the given certificate + * + * @param cert the certificated + * @return the SHA1 fingerprint + * @throws CertificateException the certificate exception + */ + @Throws(CertificateException::class) + fun generateSha1Fingerprint(cert: X509Certificate): ByteArray { + return generateFingerprint(cert, "SHA-1") + } + + /** + * Generate the fingerprint for a dedicated type. + * + * @param cert the certificate + * @param type the type + * @return the fingerprint + * @throws CertificateException certificate exception + */ + @Throws(CertificateException::class) + private fun generateFingerprint(cert: X509Certificate, type: String): ByteArray { + val fingerprint: ByteArray + val md: MessageDigest + try { + md = MessageDigest.getInstance(type) + } catch (e: Exception) { + // This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl. + throw CertificateException(e) + } + + fingerprint = md.digest(cert.encoded) + + return fingerprint + } + + /** + * Convert the fingerprint to an hexa string. + * + * @param fingerprint the fingerprint + * @return the hexa string. + */ + fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String { + val hexChars = CharArray(fingerprint.size * 3) + for (j in fingerprint.indices) { + val v = (fingerprint[j].toInt() and 0xFF) + hexChars[j * 3] = hexArray[v.ushr(4)] + hexChars[j * 3 + 1] = hexArray[v and 0x0F] + hexChars[j * 3 + 2] = sep + } + return String(hexChars, 0, hexChars.size - 1) + } + + /** + * Recursively checks the exception to see if it was caused by an + * UnrecognizedCertificateException + * + * @param root the throwable. + * @return The UnrecognizedCertificateException if exists, else null. + */ + fun getCertificateException(root: Throwable?): UnrecognizedCertificateException? { + var e = root + var i = 0 // Just in case there is a getCause loop + while (e != null && i < 10) { + if (e is UnrecognizedCertificateException) { + return e + } + e = e.cause + i++ + } + + return null + } + + internal data class PinnedSSLSocketFactory( + val sslSocketFactory: SSLSocketFactory, + val x509TrustManager: X509TrustManager + ) + + /** + * Create a SSLSocket factory for a HS config. + * + * @param hsConfig the HS config. + * @return SSLSocket factory + */ + fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): PinnedSSLSocketFactory { + try { + var defaultTrustManager: X509TrustManager? = null + + // If we haven't specified that we wanted to shouldPin the certs, fallback to standard + // X509 checks if fingerprints don't match. + if (!hsConfig.shouldPin) { + var tf: TrustManagerFactory? = null + + // get the PKIX instance + try { + tf = TrustManagerFactory.getInstance("PKIX") + } catch (e: Exception) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed") + } + + // it doesn't exist, use the default one. + if (null == tf) { + try { + tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + } catch (e: Exception) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance of default failed") + } + } + + tf!!.init(null as KeyStore?) + val trustManagers = tf.trustManagers + + for (i in trustManagers.indices) { + if (trustManagers[i] is X509TrustManager) { + defaultTrustManager = trustManagers[i] as X509TrustManager + break + } + } + } + + val trustPinned = arrayOf<TrustManager>(PinnedTrustManagerProvider.provide(hsConfig.allowedFingerprints, defaultTrustManager)) + + val sslSocketFactory: SSLSocketFactory + + if (hsConfig.forceUsageTlsVersions && hsConfig.tlsVersions != null) { + // Force usage of accepted Tls Versions for Android < 20 + sslSocketFactory = TLSSocketFactory(trustPinned, hsConfig.tlsVersions) + } else { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustPinned, java.security.SecureRandom()) + sslSocketFactory = sslContext.socketFactory + } + + return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + /** + * Create a Host name verifier for a hs config. + * + * @param hsConfig the hs config. + * @return a new HostnameVerifier. + */ + fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier { + val defaultVerifier: HostnameVerifier = OkHostnameVerifier // HttpsURLConnection.getDefaultHostnameVerifier() + val trustedFingerprints = hsConfig.allowedFingerprints + + return HostnameVerifier { hostname, session -> + if (USE_DEFAULT_HOSTNAME_VERIFIER) { + if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true + } + // TODO How to recover from this error? + if (trustedFingerprints.isEmpty()) return@HostnameVerifier false + + // If remote cert matches an allowed fingerprint, just accept it. + try { + for (cert in session.peerCertificates) { + for (allowedFingerprint in trustedFingerprints) { + if (cert is X509Certificate && allowedFingerprint.matchesCert(cert)) { + return@HostnameVerifier true + } + } + } + } catch (e: SSLPeerUnverifiedException) { + return@HostnameVerifier false + } catch (e: CertificateException) { + return@HostnameVerifier false + } + + false + } + } + + /** + * Create a list of accepted TLS specifications for a hs config. + * + * @param hsConfig the hs config. + * @return a list of accepted TLS specifications. + */ + fun newConnectionSpecs(hsConfig: HomeServerConnectionConfig): List<ConnectionSpec> { + val builder = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + val tlsVersions = hsConfig.tlsVersions + if (null != tlsVersions && tlsVersions.isNotEmpty()) { + builder.tlsVersions(*tlsVersions.toTypedArray()) + } + + val tlsCipherSuites = hsConfig.tlsCipherSuites + if (null != tlsCipherSuites && tlsCipherSuites.isNotEmpty()) { + builder.cipherSuites(*tlsCipherSuites.toTypedArray()) + } + + @Suppress("DEPRECATION") + builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions) + val list = ArrayList<ConnectionSpec>() + list.add(builder.build()) + // TODO: we should display a warning if user enter an http url + if (hsConfig.allowHttpExtension || hsConfig.homeServerUri.toString().startsWith("http://")) { + list.add(ConnectionSpec.CLEARTEXT) + } + return list + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt new file mode 100644 index 0000000000000000000000000000000000000000..f1280b879b1a45930fea986a66f0167107bbda8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +@JsonClass(generateAdapter = true) +data class Fingerprint( + val bytes: ByteArray, + val hashType: HashType +) { + + val displayableHexRepr: String by lazy { + CertUtil.fingerprintToHexString(bytes) + } + + @Throws(CertificateException::class) + internal fun matchesCert(cert: X509Certificate): Boolean { + val o: Fingerprint? = when (hashType) { + HashType.SHA256 -> newSha256Fingerprint(cert) + HashType.SHA1 -> newSha1Fingerprint(cert) + } + return equals(o) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Fingerprint + if (!bytes.contentEquals(other.bytes)) return false + if (hashType != other.hashType) return false + + return true + } + + override fun hashCode(): Int { + var result = bytes.contentHashCode() + result = 31 * result + hashType.hashCode() + return result + } + + internal companion object { + + @Throws(CertificateException::class) + fun newSha256Fingerprint(cert: X509Certificate): Fingerprint { + return Fingerprint( + CertUtil.generateSha256Fingerprint(cert), + HashType.SHA256 + ) + } + + @Throws(CertificateException::class) + fun newSha1Fingerprint(cert: X509Certificate): Fingerprint { + return Fingerprint( + CertUtil.generateSha1Fingerprint(cert), + HashType.SHA1 + ) + } + } + + @JsonClass(generateAdapter = false) + enum class HashType { + @Json(name = "sha-1") SHA1, + @Json(name = "sha-256") SHA256 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..289a4ee04f51d8674c087db44e6a82b514e2f761 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +import javax.net.ssl.X509TrustManager + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ + +/** + * @param fingerprints Not empty array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ +internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>, + private val defaultTrustManager: X509TrustManager?) : X509TrustManager { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array<X509Certificate>, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array<X509Certificate>, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + private fun checkTrusted(chain: Array<X509Certificate>) { + val cert = chain[0] + + if (!fingerprints.any { it.matchesCert(cert) }) { + throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) + } + } + + override fun getAcceptedIssuers(): Array<X509Certificate> { + return defaultTrustManager?.acceptedIssuers ?: emptyArray() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt new file mode 100644 index 0000000000000000000000000000000000000000..ed5a099ee47957d7f9a05c73635f798c7fbf7c90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import android.os.Build +import androidx.annotation.RequiresApi +import java.net.Socket +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ + +/** + * @param fingerprints An array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ +@RequiresApi(Build.VERSION_CODES.N) +internal class PinnedTrustManagerApi24(private val fingerprints: List<Fingerprint>, + private val defaultTrustManager: X509ExtendedTrustManager?) : X509ExtendedTrustManager() { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array<X509Certificate>, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + private fun checkTrusted(chain: Array<X509Certificate>) { + val cert = chain[0] + + if (!fingerprints.any { it.matchesCert(cert) }) { + throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) + } + } + + override fun getAcceptedIssuers(): Array<X509Certificate> { + return defaultTrustManager?.acceptedIssuers ?: emptyArray() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..7dcff0294f04bd3e3847d6a92984cca435d1ac68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import android.os.Build +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager + +internal object PinnedTrustManagerProvider { + // Set to false to perform some tests + private const val USE_DEFAULT_TRUST_MANAGER = true + + fun provide(fingerprints: List<Fingerprint>?, + defaultTrustManager: X509TrustManager?): X509TrustManager { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && defaultTrustManager is X509ExtendedTrustManager) { + PinnedTrustManagerApi24( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } else { + PinnedTrustManager( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ebeafd0c3e7abd882b463a7888a2ad8234cdeed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import okhttp3.TlsVersion +import timber.log.Timber +import java.io.IOException +import java.net.InetAddress +import java.net.Socket +import java.net.UnknownHostException +import java.security.KeyManagementException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager + +/** + * Force the usage of Tls versions on every created socket + * Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ + */ + +internal class TLSSocketFactory + +/** + * Constructor + * + * @param trustPinned + * @param acceptedTlsVersions + * @throws KeyManagementException + * @throws NoSuchAlgorithmException + */ +@Throws(KeyManagementException::class, NoSuchAlgorithmException::class) +constructor(trustPinned: Array<TrustManager>, acceptedTlsVersions: List<TlsVersion>) : SSLSocketFactory() { + + private val internalSSLSocketFactory: SSLSocketFactory + private val enabledProtocols: Array<String> + + init { + val context = SSLContext.getInstance("TLS") + context.init(null, trustPinned, SecureRandom()) + internalSSLSocketFactory = context.socketFactory + enabledProtocols = Array(acceptedTlsVersions.size) { + acceptedTlsVersions[it].javaName + } + } + + override fun getDefaultCipherSuites(): Array<String> { + return internalSSLSocketFactory.defaultCipherSuites + } + + override fun getSupportedCipherSuites(): Array<String> { + return internalSSLSocketFactory.supportedCipherSuites + } + + @Throws(IOException::class) + override fun createSocket(): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()) + } + + @Throws(IOException::class) + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)) + } + + @Throws(IOException::class) + override fun createSocket(host: InetAddress, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class) + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)) + } + + private fun enableTLSOnSocket(socket: Socket?): Socket? { + if (socket is SSLSocket) { + val supportedProtocols = socket.supportedProtocols.toSet() + val filteredEnabledProtocols = enabledProtocols.filter { it in supportedProtocols } + + if (filteredEnabledProtocols.isNotEmpty()) { + try { + socket.enabledProtocols = filteredEnabledProtocols.toTypedArray() + } catch (e: Exception) { + Timber.e(e) + } + } + } + return socket + } + + companion object { + private val LOG_TAG = TLSSocketFactory::class.java.simpleName + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt new file mode 100644 index 0000000000000000000000000000000000000000..ba68a51344c124c2b3db210e8ebcc1defbd6c166 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +/** + * Thrown when we are given a certificate that does match the certificate we were told to + * expect. + */ +internal data class UnrecognizedCertificateException( + val certificate: X509Certificate, + val fingerprint: Fingerprint, + override val cause: Throwable? +) : CertificateException("Unrecognized certificate with unknown fingerprint: " + certificate.subjectDN, cause) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1e20aaa5861941dc0c8b30fc0abcb1a6f0b50a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.token + +internal interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bb0f34222fb57eb1f2a5e7311882e291aa8869b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.token + +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.di.SessionId +import javax.inject.Inject + +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..6e0cb3e67782fa352f60a5db8aa75986149a5a4d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.query + +import io.realm.RealmObject +import io.realm.RealmQuery + +fun <T : RealmObject, E : Enum<E>> RealmQuery<T>.process(field: String, enums: List<Enum<E>>): RealmQuery<T> { + val lastEnumValue = enums.lastOrNull() + beginGroup() + for (enumValue in enums) { + equalTo(field, enumValue.name) + if (enumValue != lastEnumValue) { + or() + } + } + endGroup() + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..fadea5ada74e75a43f921cc4380bbdf23dd4f9ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.query + +import org.matrix.android.sdk.api.query.QueryStringValue +import io.realm.Case +import io.realm.RealmObject +import io.realm.RealmQuery +import timber.log.Timber + +fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> { + when (queryStringValue) { + is QueryStringValue.NoCondition -> Timber.v("No condition to process") + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + return this +} + +private fun QueryStringValue.Case.toRealmCase(): Case { + return when (this) { + QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt new file mode 100644 index 0000000000000000000000000000000000000000..97ebe943ec46a273111d3effe916b797a53708c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import arrow.core.Try +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.di.CacheDirectory +import org.matrix.android.sdk.internal.di.ExternalFilesDirectory +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress +import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.toCancelable +import org.matrix.android.sdk.internal.util.writeToFile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URLEncoder +import javax.inject.Inject + +internal class DefaultFileService @Inject constructor( + private val context: Context, + @CacheDirectory + private val cacheDirectory: File, + @ExternalFilesDirectory + private val externalFilesDirectory: File?, + @SessionDownloadsDirectory + private val sessionCacheDirectory: File, + private val contentUrlResolver: ContentUrlResolver, + @UnauthenticatedWithCertificateWithProgress + private val okHttpClient: OkHttpClient, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : FileService { + + private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) + + private val downloadFolder = File(sessionCacheDirectory, "MF") + + /** + * Retain ongoing downloads to avoid re-downloading and already downloading file + * map of mxCurl to callbacks + */ + private val ongoing = mutableMapOf<String, ArrayList<MatrixCallback<File>>>() + + /** + * Download file in the cache folder, and eventually decrypt it + * TODO looks like files are copied 3 times + */ + override fun downloadFile(downloadMode: FileService.DownloadMode, + id: String, + fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback<File>): Cancelable { + val unwrappedUrl = url ?: return NoOpCancellable.also { + callback.onFailure(IllegalArgumentException("url is null")) + } + + Timber.v("## FileService downloadFile $unwrappedUrl") + + synchronized(ongoing) { + val existing = ongoing[unwrappedUrl] + if (existing != null) { + Timber.v("## FileService downloadFile is already downloading.. ") + existing.add(callback) + return NoOpCancellable + } else { + // mark as tracked + ongoing[unwrappedUrl] = ArrayList() + // and proceed to download + } + } + + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { + Try { + if (!downloadFolder.exists()) { + downloadFolder.mkdirs() + } + // ensure we use unique file name by using URL (mapped to suitable file name) + // Also we need to add extension for the FileProvider, if not it lot's of app that it's + // shared with will not function well (even if mime type is passed in the intent) + File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) + }.flatMap { destFile -> + if (!destFile.exists()) { + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) + + val request = Request.Builder() + .url(resolvedUrl) + .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) + .build() + + val response = try { + okHttpClient.newCall(request).execute() + } catch (e: Throwable) { + return@flatMap Try.Failure(e) + } + + if (!response.isSuccessful) { + return@flatMap Try.Failure(IOException()) + } + + val source = response.body?.source() + ?: return@flatMap Try.Failure(IOException()) + + Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") + + if (elementToDecrypt != null) { + Timber.v("## decrypt file") + val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt) + response.close() + if (decryptedStream == null) { + return@flatMap Try.Failure(IllegalStateException("Decryption error")) + } else { + decryptedStream.use { + writeToFile(decryptedStream, destFile) + } + } + } else { + writeToFile(source.inputStream(), destFile) + response.close() + } + } + + Try.just(copyFile(destFile, downloadMode)) + } + }.fold({ + callback.onFailure(it) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + toNotify?.forEach { otherCallbacks -> + tryThis { otherCallbacks.onFailure(it) } + } + }, { file -> + callback.onSuccess(file) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") + toNotify?.forEach { otherCallbacks -> + tryThis { otherCallbacks.onSuccess(file) } + } + }) + }.toCancelable() + } + + fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { + val file = File(downloadFolder, fileForUrl(url, mimeType)) + val source = inputStream.source().buffer() + file.sink().buffer().let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } + } + } + } + + private fun fileForUrl(url: String, mimeType: String?): String { + val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } + return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() + } + + override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { + return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + } + + override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { + if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE + val isDownloading = synchronized(ongoing) { + ongoing[mxcUrl] != null + } + return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN + } + + /** + * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION + * (if not other app won't be able to access it) + */ + override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? { + // this string could be extracted no? + val authority = "${context.packageName}.mx-sdk.fileprovider" + val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) + if (!targetFile.exists()) return null + return FileProvider.getUriForFile(context, authority, targetFile) + } + + private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { + // TODO some of this seems outdated, will need to be re-worked + return when (downloadMode) { + FileService.DownloadMode.TO_EXPORT -> + file.copyTo(File(externalFilesDirectory, file.name), true) + FileService.DownloadMode.FOR_EXTERNAL_SHARE -> + file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true) + FileService.DownloadMode.FOR_INTERNAL_USE -> + file + } + } + + override fun getCacheSize(): Int { + return downloadFolder.walkTopDown() + .onEnter { + Timber.v("Get size of ${it.absolutePath}") + true + } + .sumBy { it.length().toInt() } + } + + override fun clearCache() { + downloadFolder.deleteRecursively() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff1f44341b75e3cbf76a01fc748238b34e68b1ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService { + + private val status = MutableLiveData<InitialSyncProgressService.Status>() + + private var rootTask: TaskInfo? = null + + override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status> { + return status + } + + fun startTask(@StringRes nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) { + // Create a rootTask, or add a child to the leaf + if (rootTask == null) { + rootTask = TaskInfo(nameRes, totalProgress) + } else { + val currentLeaf = rootTask!!.leaf() + + val newTask = TaskInfo(nameRes, + totalProgress, + currentLeaf, + parentWeight) + + currentLeaf.child = newTask + } + reportProgress(0) + } + + fun reportProgress(progress: Int) { + rootTask?.leaf()?.setProgress(progress) + } + + fun endTask(nameRes: Int) { + val endedTask = rootTask?.leaf() + if (endedTask?.nameRes == nameRes) { + // close it + val parent = endedTask.parent + parent?.child = null + parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt()) + } + if (endedTask?.parent == null) { + status.postValue(InitialSyncProgressService.Status.Idle) + } + } + + fun endAll() { + rootTask = null + status.postValue(InitialSyncProgressService.Status.Idle) + } + + private inner class TaskInfo(@StringRes var nameRes: Int, + var totalProgress: Int, + var parent: TaskInfo? = null, + var parentWeight: Float = 1f, + var offset: Int = parent?.currentProgress ?: 0) { + var child: TaskInfo? = null + var currentProgress: Int = 0 + + /** + * Get the further child + */ + fun leaf(): TaskInfo { + var last = this + while (last.child != null) { + last = last.child!! + } + return last + } + + /** + * Set progress of the parent if any (which will post value), or post the value + */ + fun setProgress(progress: Int) { + currentProgress = progress +// val newProgress = Math.min(currentProgress + progress, totalProgress) + parent?.let { + val parentProgress = (currentProgress * parentWeight).toInt() + it.setProgress(offset + parentProgress) + } ?: run { + Timber.v("--- ${leaf().nameRes}: $currentProgress") + status.postValue(InitialSyncProgressService.Status.Progressing(leaf().nameRes, currentProgress)) + } + } + } +} + +inline fun <T> reportSubtask(reporter: DefaultInitialSyncProgressService?, + @StringRes nameRes: Int, + totalProgress: Int, + parentWeight: Float = 1f, + block: () -> T): T { + reporter?.startTask(nameRes, totalProgress, parentWeight) + return block().also { + reporter?.endTask(nameRes) + } +} + +inline fun <K, V, R> Map<out K, V>.mapWithProgress(reporter: DefaultInitialSyncProgressService?, + taskId: Int, + weight: Float, + transform: (Map.Entry<K, V>) -> R): List<R> { + val total = count().toFloat() + var current = 0 + reporter?.startTask(taskId, 100, weight) + return map { + reporter?.reportProgress((current / total * 100).toInt()) + current++ + transform.invoke(it) + }.also { + reporter?.endTask(taskId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb4b475b4d90056041f2bef0118cf0c217e76e3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import androidx.annotation.MainThread +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.session.sync.job.SyncThread +import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.createUIHandler +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider + +@SessionScope +internal class DefaultSession @Inject constructor( + override val sessionParams: SessionParams, + private val workManagerProvider: WorkManagerProvider, + private val eventBus: EventBus, + @SessionId + override val sessionId: String, + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>, + private val sessionListeners: SessionListeners, + private val roomService: Lazy<RoomService>, + private val roomDirectoryService: Lazy<RoomDirectoryService>, + private val groupService: Lazy<GroupService>, + private val userService: Lazy<UserService>, + private val filterService: Lazy<FilterService>, + private val cacheService: Lazy<CacheService>, + private val signOutService: Lazy<SignOutService>, + private val pushRuleService: Lazy<PushRuleService>, + private val pushersService: Lazy<PushersService>, + private val termsService: Lazy<TermsService>, + private val cryptoService: Lazy<DefaultCryptoService>, + private val defaultFileService: Lazy<FileService>, + private val secureStorageService: Lazy<SecureStorageService>, + private val profileService: Lazy<ProfileService>, + private val widgetService: Lazy<WidgetService>, + private val syncThreadProvider: Provider<SyncThread>, + private val contentUrlResolver: ContentUrlResolver, + private val syncTokenStore: SyncTokenStore, + private val sessionParamsStore: SessionParamsStore, + private val contentUploadProgressTracker: ContentUploadStateTracker, + private val typingUsersTracker: TypingUsersTracker, + private val contentDownloadStateTracker: ContentDownloadStateTracker, + private val initialSyncProgressService: Lazy<InitialSyncProgressService>, + private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>, + private val accountDataService: Lazy<AccountDataService>, + private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>, + private val accountService: Lazy<AccountService>, + private val timelineEventDecryptor: TimelineEventDecryptor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val defaultIdentityService: DefaultIdentityService, + private val integrationManagerService: IntegrationManagerService, + private val taskExecutor: TaskExecutor, + private val callSignalingService: Lazy<CallSignalingService>, + @UnauthenticatedWithCertificate + private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient> +) : Session, + RoomService by roomService.get(), + RoomDirectoryService by roomDirectoryService.get(), + GroupService by groupService.get(), + UserService by userService.get(), + SignOutService by signOutService.get(), + FilterService by filterService.get(), + PushRuleService by pushRuleService.get(), + PushersService by pushersService.get(), + TermsService by termsService.get(), + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), + ProfileService by profileService.get(), + AccountDataService by accountDataService.get(), + AccountService by accountService.get() { + + override val sharedSecretStorageService: SharedSecretStorageService + get() = _sharedSecretStorageService.get() + + private var isOpen = false + + private var syncThread: SyncThread? = null + + private val uiHandler = createUIHandler() + + override val isOpenable: Boolean + get() = sessionParamsStore.get(sessionId)?.isTokenValid ?: false + + @MainThread + override fun open() { + assert(!isOpen) + isOpen = true + cryptoService.get().ensureDevice() + uiHandler.post { + lifecycleObservers.forEach { it.onStart() } + } + eventBus.register(this) + timelineEventDecryptor.start() + } + + override fun requireBackgroundSync() { + SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) + } + + override fun startAutomaticBackgroundSync(repeatDelay: Long) { + SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) + } + + override fun stopAnyBackgroundSync() { + SyncWorker.stopAnyBackgroundSync(workManagerProvider) + } + + override fun startSync(fromForeground: Boolean) { + Timber.i("Starting sync thread") + assert(isOpen) + val localSyncThread = getSyncThread() + localSyncThread.setInitialForeground(fromForeground) + if (!localSyncThread.isAlive) { + localSyncThread.start() + } else { + localSyncThread.restart() + Timber.w("Attempt to start an already started thread") + } + } + + override fun stopSync() { + assert(isOpen) + syncThread?.kill() + syncThread = null + } + + override fun close() { + assert(isOpen) + stopSync() + timelineEventDecryptor.destroy() + uiHandler.post { + lifecycleObservers.forEach { it.onStop() } + } + cryptoService.get().close() + isOpen = false + eventBus.unregister(this) + } + + override fun getSyncStateLive() = getSyncThread().liveState() + + override fun getSyncState() = getSyncThread().currentState() + + override fun hasAlreadySynced(): Boolean { + return syncTokenStore.getLastToken() != null + } + + private fun getSyncThread(): SyncThread { + return syncThread ?: syncThreadProvider.get().also { + syncThread = it + } + } + + override fun clearCache(callback: MatrixCallback<Unit>) { + stopSync() + stopAnyBackgroundSync() + uiHandler.post { + lifecycleObservers.forEach { it.onClearCache() } + } + cacheService.get().clearCache(callback) + workManagerProvider.cancelAllWorks() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onGlobalError(globalError: GlobalError) { + if (globalError is GlobalError.InvalidToken + && globalError.softLogout) { + // Mark the token has invalid + taskExecutor.executorScope.launch(Dispatchers.IO) { + sessionParamsStore.setTokenInvalid(sessionId) + } + } + + sessionListeners.dispatchGlobalError(globalError) + } + + override fun contentUrlResolver() = contentUrlResolver + + override fun contentUploadProgressTracker() = contentUploadProgressTracker + + override fun typingUsersTracker() = typingUsersTracker + + override fun contentDownloadProgressTracker(): ContentDownloadStateTracker = contentDownloadStateTracker + + override fun cryptoService(): CryptoService = cryptoService.get() + + override fun identityService() = defaultIdentityService + + override fun fileService(): FileService = defaultFileService.get() + + override fun widgetService(): WidgetService = widgetService.get() + + override fun integrationManagerService() = integrationManagerService + + override fun callSignalingService(): CallSignalingService = callSignalingService.get() + + override fun getOkHttpClient(): OkHttpClient { + return unauthenticatedWithCertificateOkHttpClient.get() + } + + override fun addListener(listener: Session.Listener) { + sessionListeners.addListener(listener) + } + + override fun removeListener(listener: Session.Listener) { + sessionListeners.removeListener(listener) + } + + // For easy debugging + override fun toString(): String { + return "$myUserId - ${sessionParams.deviceId}" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..85d714698f7b9b36d0e871e76f8699596b62a18f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.EventInsertType +import io.realm.Realm + +internal interface EventInsertLiveProcessor { + + fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean + + suspend fun process(realm: Realm, event: Event) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..475450837e0440cd6d9db85010d5574f99bd123e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker +import org.matrix.android.sdk.internal.crypto.CryptoModule +import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker +import org.matrix.android.sdk.internal.crypto.SendGossipWorker +import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.account.AccountModule +import org.matrix.android.sdk.internal.session.cache.CacheModule +import org.matrix.android.sdk.internal.session.call.CallModule +import org.matrix.android.sdk.internal.session.content.ContentModule +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.filter.FilterModule +import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker +import org.matrix.android.sdk.internal.session.group.GroupModule +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule +import org.matrix.android.sdk.internal.session.identity.IdentityModule +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule +import org.matrix.android.sdk.internal.session.openid.OpenIdModule +import org.matrix.android.sdk.internal.session.profile.ProfileModule +import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker +import org.matrix.android.sdk.internal.session.pushers.PushersModule +import org.matrix.android.sdk.internal.session.room.RoomModule +import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker +import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker +import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker +import org.matrix.android.sdk.internal.session.room.send.SendEventWorker +import org.matrix.android.sdk.internal.session.signout.SignOutModule +import org.matrix.android.sdk.internal.session.sync.SyncModule +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.session.terms.TermsModule +import org.matrix.android.sdk.internal.session.user.UserModule +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule +import org.matrix.android.sdk.internal.session.widgets.WidgetModule +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +@Component(dependencies = [MatrixComponent::class], + modules = [ + SessionModule::class, + RoomModule::class, + SyncModule::class, + HomeServerCapabilitiesModule::class, + SignOutModule::class, + GroupModule::class, + UserModule::class, + FilterModule::class, + GroupModule::class, + ContentModule::class, + CacheModule::class, + CryptoModule::class, + PushersModule::class, + OpenIdModule::class, + WidgetModule::class, + IntegrationManagerModule::class, + IdentityModule::class, + TermsModule::class, + AccountDataModule::class, + ProfileModule::class, + SessionAssistedInjectModule::class, + AccountModule::class, + CallModule::class + ] +) +@SessionScope +internal interface SessionComponent { + + fun coroutineDispatchers(): MatrixCoroutineDispatchers + + fun session(): Session + + fun syncTask(): SyncTask + + fun syncTokenStore(): SyncTokenStore + + fun networkConnectivityChecker(): NetworkConnectivityChecker + + fun taskExecutor(): TaskExecutor + + fun inject(worker: SendEventWorker) + + fun inject(worker: SendRelationWorker) + + fun inject(worker: EncryptEventWorker) + + fun inject(worker: MultipleEventSendingDispatcherWorker) + + fun inject(worker: RedactEventWorker) + + fun inject(worker: GetGroupDataWorker) + + fun inject(worker: UploadContentWorker) + + fun inject(worker: SyncWorker) + + fun inject(worker: AddHttpPusherWorker) + + fun inject(worker: SendVerificationMessageWorker) + + fun inject(worker: SendGossipRequestWorker) + + fun inject(worker: CancelGossipRequestWorker) + + fun inject(worker: SendGossipWorker) + + @Component.Factory + interface Factory { + fun create( + matrixComponent: MatrixComponent, + @BindsInstance sessionParams: SessionParams): SessionComponent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..3cc73599ff99c4d09b5f4827752e44d774c29785 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import androidx.annotation.MainThread + +/** + * This defines methods associated with some lifecycle events of a session. + * A list of SessionLifecycle will be injected into [DefaultSession] + */ +internal interface SessionLifecycleObserver { + /* + Called when the session is opened + */ + @MainThread + fun onStart() { + // noop + } + + /* + Called when the session is cleared + */ + @MainThread + fun onClearCache() { + // noop + } + + /* + Called when the session is closed + */ + @MainThread + fun onStop() { + // noop + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt new file mode 100644 index 0000000000000000000000000000000000000000..36242616fff5f33f73fbe30696c2aceb6787ec05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +internal class SessionListeners @Inject constructor() { + + private val listeners = mutableSetOf<Session.Listener>() + + fun addListener(listener: Session.Listener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + fun removeListener(listener: Session.Listener) { + synchronized(listeners) { + listeners.remove(listener) + } + } + + fun dispatchGlobalError(globalError: GlobalError) { + synchronized(listeners) { + listeners.forEach { + it.onGlobalError(globalError) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c1804ba5f276f839d87f2f8c5b2c4bd9be99e2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -0,0 +1,361 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import android.content.Context +import android.os.Build +import com.zhuinden.monarchy.Monarchy +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater +import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService +import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor +import org.matrix.android.sdk.internal.database.DatabaseCleaner +import org.matrix.android.sdk.internal.database.EventInsertLiveObserver +import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory +import org.matrix.android.sdk.internal.di.Authenticated +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.eventbus.EventBusTimberLogger +import org.matrix.android.sdk.internal.network.DefaultNetworkConnectivityChecker +import org.matrix.android.sdk.internal.network.FallbackNetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.NetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.network.PreferredNetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider +import org.matrix.android.sdk.internal.session.call.CallEventProcessor +import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor +import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService +import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor +import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor +import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor +import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor +import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultAccountDataService +import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter +import org.matrix.android.sdk.internal.util.md5 +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus +import retrofit2.Retrofit +import java.io.File +import javax.inject.Provider +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class MockHttpInterceptor + +@Module +internal abstract class SessionModule { + + @Module + companion object { + internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" + + /** + * Rules: + * Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic. + */ + + @JvmStatic + @Provides + fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig { + return sessionParams.homeServerConnectionConfig + } + + @JvmStatic + @Provides + fun providesCredentials(sessionParams: SessionParams): Credentials { + return sessionParams.credentials + } + + @JvmStatic + @UserId + @Provides + @SessionScope + fun providesUserId(credentials: Credentials): String { + return credentials.userId + } + + @JvmStatic + @DeviceId + @Provides + fun providesDeviceId(credentials: Credentials): String? { + return credentials.deviceId + } + + @JvmStatic + @UserMd5 + @Provides + @SessionScope + fun providesUserMd5(@UserId userId: String): String { + return userId.md5() + } + + @JvmStatic + @SessionId + @Provides + @SessionScope + fun providesSessionId(credentials: Credentials): String { + return credentials.sessionId() + } + + @JvmStatic + @Provides + @SessionFilesDirectory + fun providesFilesDir(@UserMd5 userMd5: String, + @SessionId sessionId: String, + context: Context): File { + // Temporary code for migration + val old = File(context.filesDir, userMd5) + if (old.exists()) { + old.renameTo(File(context.filesDir, sessionId)) + } + + return File(context.filesDir, sessionId) + } + + @JvmStatic + @Provides + @SessionDownloadsDirectory + fun providesCacheDir(@SessionId sessionId: String, + context: Context): File { + return File(context.cacheDir, "downloads/$sessionId") + } + + @JvmStatic + @Provides + @SessionDatabase + @SessionScope + fun providesRealmConfiguration(realmConfigurationFactory: SessionRealmConfigurationFactory): RealmConfiguration { + return realmConfigurationFactory.create() + } + + @JvmStatic + @Provides + @SessionDatabase + @SessionScope + fun providesMonarchy(@SessionDatabase realmConfiguration: RealmConfiguration): Monarchy { + return Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .build() + } + + @JvmStatic + @Provides + @SessionScope + @UnauthenticatedWithCertificate + fun providesOkHttpClientWithCertificate(@Unauthenticated okHttpClient: OkHttpClient, + homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } + + @JvmStatic + @Provides + @SessionScope + @Authenticated + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + @Authenticated accessTokenProvider: AccessTokenProvider, + @SessionId sessionId: String, + @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .apply { + if (testInterceptor != null) { + testInterceptor.sessionId = sessionId + addInterceptor(testInterceptor) + } + } + .build() + } + + @JvmStatic + @Provides + @SessionScope + @UnauthenticatedWithCertificateWithProgress + fun providesProgressOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + downloadProgressInterceptor: DownloadProgressInterceptor): OkHttpClient { + return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(downloadProgressInterceptor) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + }.build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesRetrofit(@Authenticated okHttpClient: Lazy<OkHttpClient>, + sessionParams: SessionParams, + retrofitFactory: RetrofitFactory): Retrofit { + return retrofitFactory + .create(okHttpClient, sessionParams.homeServerConnectionConfig.homeServerUri.toString()) + } + + @JvmStatic + @Provides + @SessionScope + fun providesEventBus(): EventBus { + return EventBus + .builder() + .logger(EventBusTimberLogger()) + .build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider<FallbackNetworkCallbackStrategy>, + preferredNetworkCallbackStrategy: Provider<PreferredNetworkCallbackStrategy> + ): NetworkCallbackStrategy { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + preferredNetworkCallbackStrategy.get() + } else { + fallbackNetworkCallbackStrategy.get() + } + } + + @JvmStatic + @Provides + @SessionScope + fun providesMxCryptoConfig(matrixConfiguration: MatrixConfiguration): MXCryptoConfig { + return matrixConfiguration.cryptoConfig + } + } + + @Binds + @Authenticated + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindSession(session: DefaultSession): Session + + @Binds + abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker + + @Binds + @IntoSet + abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindRoomTombstoneEventProcessor(processor: RoomTombstoneEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindIntegrationManager(observer: IntegrationManager): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindWidgetUrlFormatter(observer: DefaultWidgetURLFormatter): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindShieldTrustUpdated(observer: ShieldTrustUpdater): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver + + @Binds + abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService + + @Binds + abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService + + @Binds + abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + + @Binds + abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService + + @Binds + abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService + + @Binds + abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt new file mode 100644 index 0000000000000000000000000000000000000000..1fb950bad46b7c1e4c5f97bbf94389056a99e580 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import javax.inject.Scope + +@Scope +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf8701ab86712ff25ce58ab0ae246c3476f99bb9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import okhttp3.Interceptor + +interface TestInterceptor : Interceptor { + var sessionId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..be25c680a5a0ffb9796c0dd9630d73ed8fe7e5d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface AccountAPI { + + /** + * Ask the homeserver to change the password with the provided new password. + * @param params parameters to change password. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") + fun changePassword(@Body params: ChangePasswordParams): Call<Unit> + + /** + * Deactivate the user account + * + * @param params the deactivate account params + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate") + fun deactivate(@Body params: DeactivateAccountParams): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..50469f99b03a5a81e6d8016c9e6230069930fb0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class AccountModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesAccountAPI(retrofit: Retrofit): AccountAPI { + return retrofit.create(AccountAPI::class.java) + } + } + + @Binds + abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask + + @Binds + abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask + + @Binds + abstract fun bindAccountService(service: DefaultAccountService): AccountService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..347e39ae391022b543c9ec309adc4e0e81efebcb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth + +/** + * Class to pass request parameters to update the password. + */ +@JsonClass(generateAdapter = true) +internal data class ChangePasswordParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + @Json(name = "new_password") + val newPassword: String? = null +) { + companion object { + fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams { + return ChangePasswordParams( + auth = UserPasswordAuth(user = userId, password = oldPassword), + newPassword = newPassword + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9338f58e6fe5fb5ae9d9ff5308194953d116c2c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> { + data class Params( + val password: String, + val newPassword: String + ) +} + +internal class DefaultChangePasswordTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String +) : ChangePasswordTask { + + override suspend fun execute(params: ChangePasswordTask.Params) { + val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword) + try { + executeRequest<Unit>(eventBus) { + apiCall = accountAPI.changePassword(changePasswordParams) + } + } catch (throwable: Throwable) { + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + + if (registrationFlowResponse != null + /* Avoid infinite loop */ + && changePasswordParams.auth?.session == null) { + // Retry with authentication + executeRequest<Unit>(eventBus) { + apiCall = accountAPI.changePassword( + changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session)) + ) + } + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8fa999c98120a076d94f54d48656619abb5ddc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth + +@JsonClass(generateAdapter = true) +internal data class DeactivateAccountParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + // Set to true to erase all data of the account + @Json(name = "erase") + val erase: Boolean +) { + companion object { + fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + return DeactivateAccountParams( + auth = UserPasswordAuth(user = userId, password = password), + erase = erase + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b31a0a4c97e18caf1a90df46d75908e2428cedc4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.cleanup.CleanupSession +import org.matrix.android.sdk.internal.session.identity.IdentityDisconnectTask +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> { + data class Params( + val password: String, + val eraseAllData: Boolean + ) +} + +internal class DefaultDeactivateAccountTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String, + private val identityDisconnectTask: IdentityDisconnectTask, + private val cleanupSession: CleanupSession +) : DeactivateAccountTask { + + override suspend fun execute(params: DeactivateAccountTask.Params) { + val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + + executeRequest<Unit>(eventBus) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + + // Logout from identity server if any, ignoring errors + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + cleanupSession.handle() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt new file mode 100644 index 0000000000000000000000000000000000000000..892d91fe3402678ee0134c37fd355cdbec1938a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask, + private val deactivateAccountTask: DeactivateAccountTask, + private val taskExecutor: TaskExecutor) : AccountService { + + override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable { + return changePasswordTask + .configureWith(ChangePasswordTask.Params(password, newPassword)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable { + return deactivateAccountTask + .configureWith(DeactivateAccountTask.Params(password, eraseAllData)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6e2a8d0c4c586d5cef41ee79820b79ecf8d1904 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.cache + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.RealmConfiguration + +@Module +internal abstract class CacheModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionDatabase + fun providesClearCacheTask(@SessionDatabase realmConfiguration: RealmConfiguration): ClearCacheTask { + return RealmClearCacheTask(realmConfiguration) + } + } + + @Binds + abstract fun bindCacheService(service: DefaultCacheService): CacheService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b968f3d03e1489fafb50f96c9ad8ef49de76102 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.cache + +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.task.Task +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal interface ClearCacheTask : Task<Unit, Unit> + +internal class RealmClearCacheTask @Inject constructor(private val realmConfiguration: RealmConfiguration) : ClearCacheTask { + + override suspend fun execute(params: Unit) { + awaitTransaction(realmConfiguration) { + it.deleteAll() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab53e87067aa60b4e05c7e80b5435735d01cdba4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.cache + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultCacheService @Inject constructor(@SessionDatabase + private val clearCacheTask: ClearCacheTask, + private val taskExecutor: TaskExecutor) : CacheService { + + override fun clearCache(callback: MatrixCallback<Unit>) { + taskExecutor.cancelAll() + clearCacheTask + .configureWith { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d1d5808e3071c522792ddcbc62c0b0337526db0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class CallEventProcessor @Inject constructor( + @UserId private val userId: String, + private val callService: DefaultCallSignalingService +) : EventInsertLiveProcessor { + + private val allowedTypes = listOf( + EventType.CALL_ANSWER, + EventType.CALL_CANDIDATES, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } + return allowedTypes.contains(eventType) + } + + override suspend fun process(realm: Realm, event: Event) { + update(realm, event) + } + + private fun update(realm: Realm, event: Event) { + val now = System.currentTimeMillis() + // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? + event.roomId ?: return Unit.also { + Timber.w("Event with no room id ${event.eventId}") + } + val age = now - (event.ageLocalTs ?: now) + if (age > 40_000) { + // To old to ring? + return + } + event.ageLocalTs + if (EventType.isCallEvent(event.getClearType())) { + callService.onCallEvent(event) + } + Timber.v("$realm : $userId") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..60887db49722ede12884f88eb488a903be38da51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class CallModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesVoipApi(retrofit: Retrofit): VoipApi { + return retrofit.create(VoipApi::class.java) + } + } + + @Binds + abstract fun bindCallSignalingService(service: DefaultCallSignalingService): CallSignalingService + + @Binds + abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c1a129733d319e434007cdc358e1550b7ba94e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.CallsListener +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.call.model.MxCallImpl +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RoomEventSender +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +@SessionScope +internal class DefaultCallSignalingService @Inject constructor( + @UserId + private val userId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val roomEventSender: RoomEventSender, + private val taskExecutor: TaskExecutor, + private val turnServerTask: GetTurnServerTask +) : CallSignalingService { + + private val callListeners = mutableSetOf<CallsListener>() + + private val activeCalls = mutableListOf<MxCall>() + + private var cachedTurnServerResponse: TurnServerResponse? = null + + override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable { + if (cachedTurnServerResponse != null) { + cachedTurnServerResponse?.let { callback.onSuccess(it) } + return NoOpCancellable + } + return turnServerTask + .configureWith(GetTurnServerTask.Params) { + this.callback = object : MatrixCallback<TurnServerResponse> { + override fun onSuccess(data: TurnServerResponse) { + cachedTurnServerResponse = data + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { + return MxCallImpl( + callId = UUID.randomUUID().toString(), + isOutgoing = true, + roomId = roomId, + userId = userId, + otherUserId = otherUserId, + isVideoCall = isVideoCall, + localEchoEventFactory = localEchoEventFactory, + roomEventSender = roomEventSender + ).also { + activeCalls.add(it) + } + } + + override fun addCallListener(listener: CallsListener) { + callListeners.add(listener) + } + + override fun removeCallListener(listener: CallsListener) { + callListeners.remove(listener) + } + + override fun getCallWithId(callId: String): MxCall? { + Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}") + return activeCalls.find { it.callId == callId } + } + + internal fun onCallEvent(event: Event) { + when (event.getClearType()) { + EventType.CALL_ANSWER -> { + event.getClearContent().toModel<CallAnswerContent>()?.let { + if (event.senderId == userId) { + // ok it's an answer from me.. is it remote echo or other session + val knownCall = getCallWithId(it.callId) + if (knownCall == null) { + Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me") + } else if (!knownCall.isOutgoing) { + // incoming call + // if it was anwsered by this session, the call state would be in Answering(or connected) state + if (knownCall.state == CallState.LocalRinging) { + // discard current call, it's answered by another of my session + onCallManageByOtherSession(it.callId) + } + } + return + } + + onCallAnswer(it) + } + } + EventType.CALL_INVITE -> { + if (event.senderId == userId) { + // Always ignore local echos of invite + return + } + event.getClearContent().toModel<CallInviteContent>()?.let { content -> + val incomingCall = MxCallImpl( + callId = content.callId ?: return@let, + isOutgoing = false, + roomId = event.roomId ?: return@let, + userId = userId, + otherUserId = event.senderId ?: return@let, + isVideoCall = content.isVideo(), + localEchoEventFactory = localEchoEventFactory, + roomEventSender = roomEventSender + ) + activeCalls.add(incomingCall) + onCallInvite(incomingCall, content) + } + } + EventType.CALL_HANGUP -> { + event.getClearContent().toModel<CallHangupContent>()?.let { content -> + + if (event.senderId == userId) { + // ok it's an answer from me.. is it remote echo or other session + val knownCall = getCallWithId(content.callId) + if (knownCall == null) { + Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me") + } else if (!knownCall.isOutgoing) { + // incoming call + if (knownCall.state == CallState.LocalRinging) { + // discard current call, it's answered by another of my session + onCallManageByOtherSession(content.callId) + } + } + return + } + + onCallHangup(content) + activeCalls.removeAll { it.callId == content.callId } + } + } + EventType.CALL_CANDIDATES -> { + if (event.senderId == userId) { + // Always ignore local echos of invite + return + } + event.getClearContent().toModel<CallCandidatesContent>()?.let { content -> + activeCalls.firstOrNull { it.callId == content.callId }?.let { + onCallIceCandidate(it, content) + } + } + } + } + } + + private fun onCallHangup(hangup: CallHangupContent) { + callListeners.toList().forEach { + tryThis { + it.onCallHangupReceived(hangup) + } + } + } + + private fun onCallAnswer(answer: CallAnswerContent) { + callListeners.toList().forEach { + tryThis { + it.onCallAnswerReceived(answer) + } + } + } + + private fun onCallManageByOtherSession(callId: String) { + callListeners.toList().forEach { + tryThis { + it.onCallManagedByOtherSession(callId) + } + } + } + + private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) { + // Ignore the invitation from current user + if (incomingCall.otherUserId == userId) return + + callListeners.toList().forEach { + tryThis { + it.onCallInviteReceived(incomingCall, invite) + } + } + } + + private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { + callListeners.toList().forEach { + tryThis { + it.onCallIceCandidateReceived(incomingCall, candidates) + } + } + } + + companion object { + const val CALL_TIMEOUT_MS = 120_000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..c38a00d1bdf2fb1e07bf859ce4eda66a46e9fa53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class GetTurnServerTask : Task<GetTurnServerTask.Params, TurnServerResponse> { + object Params +} + +internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: VoipApi, + private val eventBus: EventBus) : GetTurnServerTask() { + + override suspend fun execute(params: Params): TurnServerResponse { + return executeRequest(eventBus) { + apiCall = voipAPI.getTurnServer() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea2f55cf67094b8561fdfd698d442cc89053b4b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface VoipApi { + + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer") + fun getTurnServer(): Call<TurnServerResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..1e724706f3172b3ab27b97fc637a1d8f750c8503 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call.model + +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RoomEventSender +import org.webrtc.IceCandidate +import org.webrtc.SessionDescription +import timber.log.Timber + +internal class MxCallImpl( + override val callId: String, + override val isOutgoing: Boolean, + override val roomId: String, + private val userId: String, + override val otherUserId: String, + override val isVideoCall: Boolean, + private val localEchoEventFactory: LocalEchoEventFactory, + private val roomEventSender: RoomEventSender +) : MxCall { + + override var state: CallState = CallState.Idle + set(value) { + field = value + dispatchStateChange() + } + + private val listeners = mutableListOf<MxCall.StateListener>() + + override fun addListener(listener: MxCall.StateListener) { + listeners.add(listener) + } + + override fun removeListener(listener: MxCall.StateListener) { + listeners.remove(listener) + } + + private fun dispatchStateChange() { + listeners.forEach { + try { + it.onStateUpdate(this) + } catch (failure: Throwable) { + Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}") + } + } + } + + init { + if (isOutgoing) { + state = CallState.Idle + } else { + // because it's created on reception of an offer + state = CallState.LocalRinging + } + } + + override fun offerSdp(sdp: SessionDescription) { + if (!isOutgoing) return + Timber.v("## VOIP offerSdp $callId") + state = CallState.Dialing + CallInviteContent( + callId = callId, + lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, + offer = CallInviteContent.Offer(sdp = sdp.description) + ) + .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + override fun sendLocalIceCandidates(candidates: List<IceCandidate>) { + CallCandidatesContent( + callId = callId, + candidates = candidates.map { + CallCandidatesContent.Candidate( + sdpMid = it.sdpMid, + sdpMLineIndex = it.sdpMLineIndex, + candidate = it.sdp + ) + } + ) + .let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) { + // For now we don't support this flow + } + + override fun hangUp() { + Timber.v("## VOIP hangup $callId") + CallHangupContent( + callId = callId + ) + .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + state = CallState.Terminated + } + + override fun accept(sdp: SessionDescription) { + Timber.v("## VOIP accept $callId") + if (isOutgoing) return + state = CallState.Answering + CallAnswerContent( + callId = callId, + answer = CallAnswerContent.Answer(sdp = sdp.description) + ) + .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + .also { localEchoEventFactory.createLocalEcho(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt new file mode 100644 index 0000000000000000000000000000000000000000..427fb59898eee6e55b7d704925adeaf1d5bdd9dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.cleanup + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.CryptoModule +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionModule +import org.matrix.android.sdk.internal.session.cache.ClearCacheTask +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +internal class CleanupSession @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + @SessionId private val sessionId: String, + private val sessionManager: SessionManager, + private val sessionParamsStore: SessionParamsStore, + @SessionDatabase private val clearSessionDataTask: ClearCacheTask, + @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, + @SessionFilesDirectory private val sessionFiles: File, + @SessionDownloadsDirectory private val sessionCache: File, + private val realmKeysUtils: RealmKeysUtils, + @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, + @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, + @UserMd5 private val userMd5: String +) { + suspend fun handle() { + Timber.d("Cleanup: release session...") + sessionManager.releaseSession(sessionId) + + Timber.d("Cleanup: cancel pending works...") + workManagerProvider.cancelAllWorks() + + Timber.d("Cleanup: delete session params...") + sessionParamsStore.delete(sessionId) + + Timber.d("Cleanup: clear session data...") + clearSessionDataTask.execute(Unit) + + Timber.d("Cleanup: clear crypto data...") + clearCryptoDataTask.execute(Unit) + + Timber.d("Cleanup: clear file system") + sessionFiles.deleteRecursively() + sessionCache.deleteRecursively() + + Timber.d("Cleanup: clear the database keys") + realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) + realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) + + // Sanity check + if (BuildConfig.DEBUG) { + Realm.getGlobalInstanceCount(realmSessionConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for session has not been closed ($it)") } + Realm.getGlobalInstanceCount(realmCryptoConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ba87d060978fa6e33c8bb6de6f2b1bbb826cdf05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.internal.session.download.DefaultContentDownloadStateTracker + +@Module +internal abstract class ContentModule { + + @Binds + abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker + + @Binds + abstract fun bindContentDownloadStateTracker(tracker: DefaultContentDownloadStateTracker): ContentDownloadStateTracker + + @Binds + abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb0c11d1b8328bd8f0bb232c1d4b3a3d7d973a98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ContentUploadResponse( + @Json(name = "content_uri") val contentUri: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa8b98ae62d533df266ed77f543d4129e69998f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.os.Handler +import android.os.Looper +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultContentUploadStateTracker @Inject constructor() : ContentUploadStateTracker { + + private val mainHandler = Handler(Looper.getMainLooper()) + private val states = mutableMapOf<String, ContentUploadStateTracker.State>() + private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>() + + override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + val listeners = listeners.getOrPut(key) { ArrayList() } + listeners.add(updateListener) + val currentState = states[key] ?: ContentUploadStateTracker.State.Idle + mainHandler.post { + try { + updateListener.onUpdate(currentState) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + + override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + listeners[key]?.apply { + remove(updateListener) + } + } + + override fun clear() { + listeners.clear() + } + + internal fun setFailure(key: String, throwable: Throwable) { + val failure = ContentUploadStateTracker.State.Failure(throwable) + updateState(key, failure) + } + + internal fun setSuccess(key: String) { + val success = ContentUploadStateTracker.State.Success + updateState(key, success) + } + + internal fun setEncryptingThumbnail(key: String) { + val progressData = ContentUploadStateTracker.State.EncryptingThumbnail + updateState(key, progressData) + } + + internal fun setProgressThumbnail(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.UploadingThumbnail(current, total) + updateState(key, progressData) + } + + internal fun setEncrypting(key: String) { + val progressData = ContentUploadStateTracker.State.Encrypting + updateState(key, progressData) + } + + internal fun setProgress(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.Uploading(current, total) + updateState(key, progressData) + } + + private fun updateState(key: String, state: ContentUploadStateTracker.State) { + states[key] = state + mainHandler.post { + listeners[key]?.forEach { + try { + it.onUpdate(state) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..80f69f88903a5458066d085a17de9debe4b0624c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import javax.inject.Inject + +private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" + +internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { + + private val baseUrl = homeServerConnectionConfig.homeServerUri.toString().ensureTrailingSlash() + + override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" + + override fun resolveFullSize(contentUrl: String?): String? { + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/" + ) + } + } + + override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? { + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/", + params = "?width=$width&height=$height&method=${method.value}" + ) + } + } + + private fun resolve(contentUrl: String, + prefix: String, + params: String = ""): String? { + var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME) + val fragmentOffset = serverAndMediaId.indexOf("#") + var fragment = "" + if (fragmentOffset >= 0) { + fragment = serverAndMediaId.substring(fragmentOffset) + serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) + } + + return baseUrl + prefix + serverAndMediaId + params + fragment + } + + private fun String.isValidMatrixContentUrl(): Boolean { + return startsWith(MATRIX_CONTENT_URI_SCHEME) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt new file mode 100644 index 0000000000000000000000000000000000000000..798d7ceee032a538dd9be880d1f68186422d83a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.net.Uri +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.internal.di.Authenticated +import org.matrix.android.sdk.internal.network.ProgressRequestBody +import org.matrix.android.sdk.internal.network.awaitResponse +import org.matrix.android.sdk.internal.network.toFailure +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import javax.inject.Inject + +internal class FileUploader @Inject constructor(@Authenticated + private val okHttpClient: OkHttpClient, + private val eventBus: EventBus, + private val context: Context, + contentUrlResolver: ContentUrlResolver, + moshi: Moshi) { + + private val uploadUrl = contentUrlResolver.uploadUrl + private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) + + suspend fun uploadFile(file: File, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull()) + return upload(uploadBody, filename, progressListener) + } + + suspend fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull()) + return upload(uploadBody, filename, progressListener) + } + + suspend fun uploadFromUri(uri: Uri, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val inputStream = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } ?: throw FileNotFoundException() + + inputStream.use { + return uploadByteArray(it.readBytes(), filename, mimeType, progressListener) + } + } + + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { + val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() + + val httpUrl = urlBuilder + .addQueryParameter("filename", filename) + .build() + + val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody + + val request = Request.Builder() + .url(httpUrl) + .post(requestBody) + .build() + + return okHttpClient.newCall(request).awaitResponse().use { response -> + if (!response.isSuccessful) { + throw response.toFailure(eventBus) + } else { + response.body?.source()?.let { + responseAdapter.fromJson(it) + } + ?: throw IOException() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..05558fcf4caf6e8b06453b7bc767e3fec99e0db3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import timber.log.Timber +import java.io.ByteArrayOutputStream + +internal object ThumbnailExtractor { + + class ThumbnailData( + val width: Int, + val height: Int, + val size: Long, + val bytes: ByteArray, + val mimeType: String + ) + + fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + return if (attachment.type == ContentAttachmentData.Type.VIDEO) { + extractVideoThumbnail(context, attachment) + } else { + null + } + } + + private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(context, attachment.queryUri) + val thumbnail = mediaMetadataRetriever.frameAtTime + + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = "image/jpeg" + ) + thumbnail.recycle() + outputStream.reset() + } catch (e: Exception) { + Timber.e(e, "Cannot extract video thumbnail") + } finally { + mediaMetadataRetriever.release() + } + return thumbnailData + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d354cdcbeeb002104a9c4d67d23ba6a591bba72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import id.zelory.compressor.Compressor +import id.zelory.compressor.constraint.default +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.network.ProgressRequestBody +import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.UUID +import javax.inject.Inject + +private data class NewImageAttributes( + val newWidth: Int?, + val newHeight: Int?, + val newFileSize: Int +) + +/** + * Possible previous worker: None + * Possible next worker : Always [MultipleEventSendingDispatcherWorker] + */ +internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List<Event>, + val attachment: ContentAttachmentData, + val isEncrypted: Boolean, + val compressBeforeSending: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var fileUploader: FileUploader + @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker + @Inject lateinit var fileService: DefaultFileService + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + Timber.v("Starting upload media work with params $params") + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + // Just defensive code to ensure that we never have an uncaught exception that could break the queue + return try { + internalDoWork(params) + } catch (failure: Throwable) { + Timber.e(failure) + handleFailure(params, failure) + } + } + + private suspend fun internalDoWork(params: Params): Result { + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val attachment = params.attachment + + var newImageAttributes: NewImageAttributes? = null + + try { + val inputStream = context.contentResolver.openInputStream(attachment.queryUri) + ?: return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString() + ) + ) + ) + + inputStream.use { + var uploadedThumbnailUrl: String? = null + var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null + + ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } + } + } + + try { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt thumbnail") + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } + val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } + + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t, "Thumbnail update failed") + } + } + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } + } + } + } + + var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null + + return try { + // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should + // copy it to a cache folder by using InputStream and OutputStream. + // https://github.com/zetbaitsu/Compressor/pull/150 + // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. + var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) + cacheFile.parentFile?.mkdirs() + if (cacheFile.exists()) { + cacheFile.delete() + } + cacheFile.createNewFile() + cacheFile.deleteOnExit() + + val outputStream = FileOutputStream(cacheFile) + outputStream.use { + inputStream.copyTo(outputStream) + } + + if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + cacheFile = Compressor.compress(context, cacheFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + }.also { compressedFile -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } + + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt file") + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType()) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + // If it's a file update the file service so that it does not redownload? + if (params.attachment.type == ContentAttachmentData.Type.FILE) { + context.contentResolver.openInputStream(attachment.queryUri)?.let { + fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) + } + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { + Timber.e(t) + handleFailure(params, t) + } + } + } catch (e: Exception) { + Timber.e(e) + notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = e.localizedMessage + ) + ) + ) + } + } + + private fun handleFailure(params: Params, failure: Throwable): Result { + notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } + + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = failure.localizedMessage + ) + ) + ) + } + + private fun handleSuccess(params: Params, + attachmentUrl: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Result { + Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") + notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } + + val updatedEvents = params.events + .map { + updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) + } + + val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) + return Result.success(WorkerParamsFactory.toData(sendParams)) + } + + private fun updateEvent(event: Event, + url: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String? = null, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Event { + val messageContent: MessageContent = event.content.toModel() ?: return event + val updatedContent = when (messageContent) { + is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo) + is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) + else -> messageContent + } + return event.copy(content = updatedContent.toContent()) + } + + private fun notifyTracker(params: Params, function: (String) -> Unit) { + params.events + .mapNotNull { it.eventId } + .forEach { eventId -> function.invoke(eventId) } + } + + private fun MessageImageContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): MessageImageContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url), + info = info?.copy( + width = newImageAttributes?.newWidth ?: info.width, + height = newImageAttributes?.newHeight ?: info.height, + size = newImageAttributes?.newFileSize ?: info.size + ) + ) + } + + private fun MessageVideoContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url), + videoInfo = videoInfo?.copy( + thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, + thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl) + ) + ) + } + + private fun MessageFileContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageFileContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) + } + + private fun MessageAudioContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageAudioContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) + } + + companion object { + private const val MAX_IMAGE_SIZE = 640 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..295a829b0874bc2e03279d03496f8b068090646c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.download + +import android.os.Handler +import android.os.Looper +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultContentDownloadStateTracker @Inject constructor() : ProgressListener, ContentDownloadStateTracker { + + private val mainHandler = Handler(Looper.getMainLooper()) + private val states = mutableMapOf<String, ContentDownloadStateTracker.State>() + private val listeners = mutableMapOf<String, MutableList<ContentDownloadStateTracker.UpdateListener>>() + + override fun track(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + val listeners = listeners.getOrPut(key) { ArrayList() } + if (!listeners.contains(updateListener)) { + listeners.add(updateListener) + } + val currentState = states[key] ?: ContentDownloadStateTracker.State.Idle + mainHandler.post { + try { + updateListener.onDownloadStateUpdate(currentState) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + + override fun unTrack(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + listeners[key]?.apply { + remove(updateListener) + } + } + + override fun clear() { + states.clear() + listeners.clear() + } + +// private fun URL.toKey() = toString() + + override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { + Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") + if (done) { + updateState(url, ContentDownloadStateTracker.State.Success) + } else { + updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + } + } + + override fun error(url: String, errorCode: Int) { + Timber.v("## DL Progress Error code:$errorCode") + updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + } + } + + private fun updateState(url: String, state: ContentDownloadStateTracker.State) { + states[url] = state + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(state) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8ef0c3323b63bcfcb20bdc9652ed2b10184f04a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.download + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +internal class DownloadProgressInterceptor @Inject constructor( + private val downloadStateTracker: DefaultContentDownloadStateTracker +) : Interceptor { + + companion object { + const val DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER = "matrix-sdk:mxc_URL" + } + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url.toUrl() + val mxcURl = chain.request().header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER) + + val request = chain.request().newBuilder() + .removeHeader(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER) + .build() + + val originalResponse = chain.proceed(request) + if (!originalResponse.isSuccessful) { + downloadStateTracker.error(mxcURl ?: url.toExternalForm(), originalResponse.code) + return originalResponse + } + val responseBody = originalResponse.body ?: return originalResponse + return originalResponse.newBuilder() + .body(ProgressResponseBody(responseBody, mxcURl ?: url.toExternalForm(), downloadStateTracker)) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bfe74862dbd2bb1268277b9af468afffa3fb7fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.download + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer + +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val chainUrl: String, + private val progressListener: ProgressListener) : ResponseBody() { + + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? = responseBody.contentType() + override fun contentLength(): Long = responseBody.contentLength() + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0L + progressListener.update(chainUrl, totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } + } +} + +interface ProgressListener { + fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) + fun error(url: String, errorCode: Int) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..e07778b536117021d3b966bf26307eb69712ded2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.FilterEntity +import org.matrix.android.sdk.internal.database.model.FilterEntityFields +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.where +import javax.inject.Inject + +internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository { + + override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val filterEntity = FilterEntity.get(realm) + // Filter has changed, or no filter Id yet + filterEntity == null + || filterEntity.filterBodyJson != filter.toJSONString() + || filterEntity.filterId.isBlank() + }.also { hasChanged -> + if (hasChanged) { + // Filter is new or has changed, store it and reset the filter Id. + // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread + monarchy.awaitTransaction { realm -> + // We manage only one filter for now + val filterJson = filter.toJSONString() + val roomEventFilterJson = roomEventFilter.toJSONString() + + val filterEntity = FilterEntity.getOrCreate(realm) + + filterEntity.filterBodyJson = filterJson + filterEntity.roomEventFilterJson = roomEventFilterJson + // Reset filterId + filterEntity.filterId = "" + } + } + } + } + + override suspend fun storeFilterId(filter: Filter, filterId: String) { + monarchy.awaitTransaction { + // We manage only one filter for now + val filterJson = filter.toJSONString() + + // Update the filter id, only if the filter body matches + it.where<FilterEntity>() + .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson) + ?.findFirst() + ?.filterId = filterId + } + } + + override suspend fun getFilter(): String { + return monarchy.awaitTransaction { + val filter = FilterEntity.getOrCreate(it) + if (filter.filterId.isBlank()) { + // Use the Json format + filter.filterBodyJson + } else { + // Use FilterId + filter.filterId + } + } + } + + override suspend fun getRoomFilter(): String { + return monarchy.awaitTransaction { + FilterEntity.getOrCreate(it).roomEventFilterJson + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d120312a1f6f31ebbd8022026531bde0959e66ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultFilterService @Inject constructor(private val saveFilterTask: SaveFilterTask, + private val taskExecutor: TaskExecutor) : FilterService { + + // TODO Pass a list of support events instead + override fun setFilter(filterPreset: FilterService.FilterPreset) { + saveFilterTask + .configureWith(SaveFilterTask.Params(filterPreset)) + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5c214dbaa0b4d2397eb31a3ff3a5b7e1f6db2ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +/** + * Save a filter, in db and if any changes, upload to the server + */ +internal interface SaveFilterTask : Task<SaveFilterTask.Params, Unit> { + + data class Params( + val filterPreset: FilterService.FilterPreset + ) +} + +internal class DefaultSaveFilterTask @Inject constructor( + @UserId private val userId: String, + private val filterAPI: FilterApi, + private val filterRepository: FilterRepository, + private val eventBus: EventBus +) : SaveFilterTask { + + override suspend fun execute(params: SaveFilterTask.Params) { + val filterBody = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotFilter() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultFilter() + } + } + val roomFilter = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotRoomFilter() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultRoomFilter() + } + } + val updated = filterRepository.storeFilter(filterBody, roomFilter) + if (updated) { + val filterResponse = executeRequest<FilterResponse>(eventBus) { + // TODO auto retry + apiCall = filterAPI.uploadFilter(userId, filterBody) + } + filterRepository.storeFilterId(filterBody, filterResponse.filterId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..fdfda09633db7341d8dca7c0c6dddd0e1b81e1f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "Filter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class EventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List<String>? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. + * A matching sender will be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List<String>? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List<String>? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. + * A matching type will be excluded even if it is listed in the 'types' filter. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List<String>? = null +) { + fun hasData(): Boolean { + return limit != null + || senders != null + || notSenders != null + || types != null + || notTypes != null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt new file mode 100644 index 0000000000000000000000000000000000000000..4c4a4d6181cb6b9f47d318db38686b066c31e128 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class which can be parsed to a filter json string. Used for POST and GET + * Have a look here for further information: + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +internal data class Filter( + /** + * List of event fields to include. If this list is absent then all fields are included. The entries may + * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the + * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may + * include more fields than were requested. + */ + @Json(name = "event_fields") val eventFields: List<String>? = null, + /** + * The format to use for events. 'client' will return the events in a format suitable for clients. + * 'federation' will return the raw event as received over federation. The default is 'client'. One of: ["client", "federation"] + */ + @Json(name = "event_format") val eventFormat: String? = null, + /** + * The presence updates to include. + */ + @Json(name = "presence") val presence: EventFilter? = null, + /** + * The user account data that isn't associated with rooms to include. + */ + @Json(name = "account_data") val accountData: EventFilter? = null, + /** + * Filters to be applied to room data. + */ + @Json(name = "room") val room: RoomFilter? = null +) { + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a45a6d66fdef6e855e4bed0aaa97e3f85dd1c72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Matthias Kesler + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface FilterApi { + + /** + * Upload FilterBody to get a filter_id which can be used for /sync requests + * + * @param userId the user id + * @param body the Json representation of a FilterBody object + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") + fun uploadFilter(@Path("userId") userId: String, + @Body body: Filter): Call<FilterResponse> + + /** + * Gets a filter with a given filterId from the homeserver + * + * @param userId the user id + * @param filterId the filterID + * @return Filter + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") + fun getFilterById(@Path("userId") userId: String, + @Path("filterId") filterId: String): Call<Filter> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..12764248efd88e723b2d23b0b1f94f3cf4d28e9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.events.model.EventType + +internal object FilterFactory { + + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { + return RoomEventFilter( + limit = numberOfEvents, + containsUrl = true, + types = listOf(EventType.MESSAGE), + lazyLoadMembers = true + ) + } + + fun createDefaultFilter(): Filter { + return FilterUtil.enableLazyLoading(Filter(), true) + } + + fun createRiotFilter(): Filter { + return Filter( + room = RoomFilter( + timeline = createRiotTimelineFilter(), + state = createRiotStateFilter() + ) + ) + } + + fun createDefaultRoomFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + ) + } + + fun createRiotRoomFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + // TODO Enable this for optimization + // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList() + ) + } + + private fun createRiotTimelineFilter(): RoomEventFilter { + return RoomEventFilter().apply { + // TODO Enable this for optimization + // types = listOfSupportedEventTypes.toMutableList() + } + } + + private fun createRiotStateFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + ) + } + + // Get only managed types by Riot + private val listOfSupportedEventTypes = listOf( + // TODO Complete the list + EventType.MESSAGE + ) + + // Get only managed types by Riot + private val listOfSupportedStateEventTypes = listOf( + // TODO Complete the list + EventType.STATE_ROOM_MEMBER + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5052d57ac9fc37e0547e05add847fe9eea467d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class FilterModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesFilterApi(retrofit: Retrofit): FilterApi { + return retrofit.create(FilterApi::class.java) + } + } + + @Binds + abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository + + @Binds + abstract fun bindFilterService(service: DefaultFilterService): FilterService + + @Binds + abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..b19478c42fb093757ae8785498fa3d74ad387a8f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +internal interface FilterRepository { + + /** + * Return true if the filterBody has changed, or need to be sent to the server + */ + suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean + + /** + * Set the filterId of this filter + */ + suspend fun storeFilterId(filter: Filter, filterId: String) + + /** + * Return filter json or filter id + */ + suspend fun getFilter(): String + + /** + * Return the room filter + */ + suspend fun getRoomFilter(): String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..951b7e8ca28313d4d8ee8f1543310404df6c7876 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents the body which is the response when creating a filter on the server + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class FilterResponse( + /** + * Required. The ID of the filter that was created. Cannot start with a { as this character + * is used to determine if the filter provided is inline JSON or a previously declared + * filter by homeservers on some APIs. + */ + @Json(name = "filter_id") val filterId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..3a030cc47065bee1a36d131edfdcca7895640dd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.filter + +internal object FilterUtil { + + /** + * Patch the filterBody to enable or disable the data save mode + * + * If data save mode is on, FilterBody will contains + * FIXME New expected filter: + * "{\"room\": {\"ephemeral\": {\"notTypes\": [\"m.typing\"]}}, \"presence\":{\"notTypes\": [\"*\"]}}" + * + * @param filterBody filterBody to patch + * @param useDataSaveMode true to enable data save mode + */ + /* + fun enableDataSaveMode(filterBody: FilterBody, useDataSaveMode: Boolean) { + if (useDataSaveMode) { + // Enable data save mode + if (filterBody.room == null) { + filterBody.room = RoomFilter() + } + filterBody.room?.let { room -> + if (room.ephemeral == null) { + room.ephemeral = RoomEventFilter() + } + room.ephemeral?.types?.let { types -> + if (!types.contains("m.receipt")) { + types.add("m.receipt") + } + } + } + + if (filterBody.presence == null) { + filterBody.presence = Filter() + } + filterBody.presence?.notTypes?.let { notTypes -> + if (!notTypes.contains("*")) { + notTypes.add("*") + } + } + } else { + filterBody.room?.let { room -> + room.ephemeral?.types?.remove("m.receipt") + if (room.ephemeral?.types?.isEmpty() == true) { + room.ephemeral?.types = null + } + if (room.ephemeral?.hasData() == false) { + room.ephemeral = null + } + } + if (filterBody.room?.hasData() == false) { + filterBody.room = null + } + + filterBody.presence?.let { presence -> + presence.notTypes?.remove("*") + if (presence.notTypes?.isEmpty() == true) { + presence.notTypes = null + } + } + if (filterBody.presence?.hasData() == false) { + filterBody.presence = null + } + } + } */ + + /** + * Compute a new filter to enable or disable the lazy loading + * + * + * If lazy loading is on, the filter will looks like + * {"room":{"state":{"lazy_load_members":true})} + * + * @param filter filter to patch + * @param useLazyLoading true to enable lazy loading + */ + fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter { + if (useLazyLoading) { + // Enable lazy loading + return filter.copy( + room = filter.room?.copy( + state = filter.room.state?.copy(lazyLoadMembers = true) + ?: RoomEventFilter(lazyLoadMembers = true) + ) + ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) + ) + } else { + val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } + val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } + + return filter.copy( + room = newRoomFilter + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..cefa9e8ece2abe79fa0ffafe4797bfba4c430dd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Represents "RoomEventFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class RoomEventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will + * be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List<String>? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will + * be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List<String>? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List<String>? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. A '*' can be used as + * a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List<String>? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + */ + @Json(name = "rooms") val rooms: List<String>? = null, + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded + * even if it is listed in the 'rooms' filter. + */ + @Json(name = "not_rooms") val notRooms: List<String>? = null, + /** + * If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url + * key is not considered for filtering. + */ + @Json(name = "contains_url") val containsUrl: Boolean? = null, + /** + * If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false. + */ + @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null +) { + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(RoomEventFilter::class.java).toJson(this) + } + + fun hasData(): Boolean { + return (limit != null + || notSenders != null + || notTypes != null + || senders != null + || types != null + || rooms != null + || notRooms != null + || containsUrl != null + || lazyLoadMembers != null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0694aee51bc37511fe5ecbdab4f3886b1c001c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "RoomFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class RoomFilter( + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. + * A matching room will be excluded even if it is listed in the 'rooms' filter. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ + @Json(name = "not_rooms") val notRooms: List<String>? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ + @Json(name = "rooms") val rooms: List<String>? = null, + /** + * The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. + */ + @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, + /** + * Include rooms that the user has left in the sync, default false + */ + @Json(name = "include_leave") val includeLeave: Boolean? = null, + /** + * The state events to include for rooms. + * Developer remark: StateFilter is exactly the same than RoomEventFilter + */ + @Json(name = "state") val state: RoomEventFilter? = null, + /** + * The message and state update events to include for rooms. + */ + @Json(name = "timeline") val timeline: RoomEventFilter? = null, + /** + * The per user account data to include for rooms. + */ + @Json(name = "account_data") val accountData: RoomEventFilter? = null +) { + + fun hasData(): Boolean { + return (notRooms != null + || rooms != null + || ephemeral != null + || includeLeave != null + || state != null + || timeline != null + || accountData != null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..c91bd381a471d42fde8278d13ba6831b7572b5dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.group.model.GroupRooms +import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse +import org.matrix.android.sdk.internal.session.group.model.GroupUsers +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface GetGroupDataTask : Task<GetGroupDataTask.Params, Unit> { + sealed class Params { + object FetchAllActive : Params() + data class FetchWithIds(val groupIds: List<String>) : Params() + } +} + +internal class DefaultGetGroupDataTask @Inject constructor( + private val groupAPI: GroupAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : GetGroupDataTask { + + private data class GroupData( + val groupId: String, + val groupSummary: GroupSummaryResponse, + val groupRooms: GroupRooms, + val groupUsers: GroupUsers + ) + + override suspend fun execute(params: GetGroupDataTask.Params) { + val groupIds = when (params) { + is GetGroupDataTask.Params.FetchAllActive -> { + getActiveGroupIds() + } + is GetGroupDataTask.Params.FetchWithIds -> { + params.groupIds + } + } + Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}") + val data = groupIds.map { groupId -> + val groupSummary = executeRequest<GroupSummaryResponse>(eventBus) { + apiCall = groupAPI.getSummary(groupId) + } + val groupRooms = executeRequest<GroupRooms>(eventBus) { + apiCall = groupAPI.getRooms(groupId) + } + val groupUsers = executeRequest<GroupUsers>(eventBus) { + apiCall = groupAPI.getUsers(groupId) + } + GroupData(groupId, groupSummary, groupRooms, groupUsers) + } + insertInDb(data) + } + + private fun getActiveGroupIds(): List<String> { + return monarchy.fetchAllMappedSync( + { realm -> + GroupEntity.where(realm, Membership.activeMemberships()) + }, + { it.groupId } + ) + } + + private suspend fun insertInDb(groupDataList: List<GroupData>) { + monarchy + .awaitTransaction { realm -> + groupDataList.forEach { groupData -> + + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId) + + groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: "" + val name = groupData.groupSummary.profile?.name + groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name + groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: "" + + groupSummaryEntity.roomIds.clear() + groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId } + + groupSummaryEntity.userIds.clear() + groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt new file mode 100644 index 0000000000000000000000000000000000000000..3fbed5d99222466958947c19874bd9ccc02063d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultGroup(override val groupId: String, + private val taskExecutor: TaskExecutor, + private val getGroupDataTask: GetGroupDataTask) : Group { + + override fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable { + val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) + return getGroupDataTask.configureWith(params) { + this.callback = callback + }.executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt new file mode 100644 index 0000000000000000000000000000000000000000..25c9d1dff72a98d1caa9d82269081ace364c131b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val groupFactory: GroupFactory) : GroupService { + + override fun getGroup(groupId: String): Group? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + GroupEntity.where(realm, groupId).findFirst()?.let { + groupFactory.create(groupId) + } + } + } + + override fun getGroupSummary(groupId: String): GroupSummary? { + return monarchy.fetchCopyMap( + { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() }, + { it, _ -> it.asDomain() } + ) + } + + override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> { + return monarchy.fetchAllMappedSync( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>> { + return monarchy.findAllMappedWithChanges( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery<GroupSummaryEntity> { + return GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a04f076e99c3e22aaff520edbef48988f80a819 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var getGroupDataTask: GetGroupDataTask + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + return runCatching { + getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) + }.fold( + { Result.success() }, + { Result.retry() } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5156017ea133f8b7cb78395849084198beb3246 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.group.model.GroupRooms +import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse +import org.matrix.android.sdk.internal.session.group.model.GroupUsers +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface GroupAPI { + + /** + * Request a group summary + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary") + fun getSummary(@Path("groupId") groupId: String): Call<GroupSummaryResponse> + + /** + * Request the rooms list. + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms") + fun getRooms(@Path("groupId") groupId: String): Call<GroupRooms> + + /** + * Request the users list. + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users") + fun getUsers(@Path("groupId") groupId: String): Call<GroupUsers> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9566fe5f16db413025ef18887761c340100d435 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal interface GroupFactory { + fun create(groupId: String): Group +} + +@SessionScope +internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, + private val taskExecutor: TaskExecutor) : + GroupFactory { + + override fun create(groupId: String): Group { + return DefaultGroup( + groupId = groupId, + taskExecutor = taskExecutor, + getGroupDataTask = getGroupDataTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b47bb0a5ad81843c965a165ba0f039c3eadb110b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class GroupModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesGroupAPI(retrofit: Retrofit): GroupAPI { + return retrofit.create(GroupAPI::class.java) + } + } + + @Binds + abstract fun bindGroupFactory(factory: DefaultGroupFactory): GroupFactory + + @Binds + abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask + + @Binds + abstract fun bindGroupService(service: DefaultGroupService): GroupService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt new file mode 100644 index 0000000000000000000000000000000000000000..9990e3d8211c60b3aa341fd1f34c636e88a2d7b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents a community profile in the server responses. + */ +@JsonClass(generateAdapter = true) +internal data class GroupProfile( + + @Json(name = "short_description") val shortDescription: String? = null, + + /** + * Tell whether the group is public. + */ + @Json(name = "is_public") val isPublic: Boolean? = null, + + /** + * The URL for the group's avatar. May be nil. + */ + @Json(name = "avatar_url") val avatarUrl: String? = null, + + /** + * The group's name. + */ + @Json(name = "name") val name: String? = null, + + /** + * The optional HTML formatted string used to described the group. + */ + @Json(name = "long_description") val longDescription: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt new file mode 100644 index 0000000000000000000000000000000000000000..c93878a0d4997318c7e5f4a379d2b34f20d89b20 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupRoom( + + @Json(name = "aliases") val aliases: List<String> = emptyList(), + @Json(name = "canonical_alias") val canonicalAlias: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "num_joined_members") val numJoinedMembers: Int = 0, + @Json(name = "room_id") val roomId: String, + @Json(name = "topic") val topic: String? = null, + @Json(name = "world_readable") val worldReadable: Boolean = false, + @Json(name = "guest_can_join") val guestCanJoin: Boolean = false, + @Json(name = "avatar_url") val avatarUrl: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt new file mode 100644 index 0000000000000000000000000000000000000000..f7e36ad8bc448e9363b8910fec25b1e6e7cfd69a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupRooms( + + @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null, + @Json(name = "chunk") val rooms: List<GroupRoom> = emptyList() + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..a11ba1ecdcd637f2d5c9c5a28fd0421b8407d140 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the summary of a community in the server response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryResponse( + /** + * The group profile. + */ + @Json(name = "profile") val profile: GroupProfile? = null, + + /** + * The group users. + */ + @Json(name = "users_section") val usersSection: GroupSummaryUsersSection? = null, + + /** + * The current user status. + */ + @Json(name = "user") val user: GroupSummaryUser? = null, + + /** + * The rooms linked to the community. + */ + @Json(name = "rooms_section") val roomsSection: GroupSummaryRoomsSection? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt new file mode 100644 index 0000000000000000000000000000000000000000..428caaa20991cea13f07334607c0249ad56fe768 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the community rooms in a group summary response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryRoomsSection( + + @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null, + + @Json(name = "rooms") val rooms: List<String> = emptyList() + + // @TODO: Check the meaning and the usage of these categories. This dictionary is empty FTM. + // public Map<Object, Object> categories; +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt new file mode 100644 index 0000000000000000000000000000000000000000..f61160fb1a55949b486e3612f834e9d1ebe1cd4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the current user status in a group summary response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryUser( + + /** + * The current user membership in this community. + */ + @Json(name = "membership") val membership: String? = null, + + /** + * Tell whether the user published this community on his profile. + */ + @Json(name = "is_publicised") val isPublicised: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8ade1ab5ed28abf5f9562df1ed794f36430b8e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the community members in a group summary response. + */ + +@JsonClass(generateAdapter = true) +internal data class GroupSummaryUsersSection( + + @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int, + + @Json(name = "users") val users: List<String> = emptyList() + + // @TODO: Check the meaning and the usage of these roles. This dictionary is empty FTM. + // public Map<Object, Object> roles; +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9a9631ef7ac8119da2725aa7f7b7f3946158eab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupUser( + @Json(name = "display_name") val displayName: String = "", + @Json(name = "user_id") val userId: String, + @Json(name = "is_privileged") val isPrivileged: Boolean = false, + @Json(name = "avatar_url") val avatarUrl: String? = "", + @Json(name = "is_public") val isPublic: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt new file mode 100644 index 0000000000000000000000000000000000000000..1ce283756d21c4610243ea916523fd711748e993 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupUsers( + @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int, + @Json(name = "chunk") val users: List<GroupUser> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..33ebf3e5480c42b6f65b09be3944bc93ca56f0f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface CapabilitiesAPI { + + /** + * Request the homeserver capabilities + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") + fun getCapabilities(): Call<GetCapabilitiesResult> + + /** + * Request the upload capabilities + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult> + + /** + * Request the versions + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun getVersions(): Call<Versions> + + /** + * Ping the homeserver. We do not care about the returned data, so there is no use to parse them + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun ping(): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f9d9548d226514591e795924a350d7dc91309d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.wellknown.GetWellknownTask +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import java.util.Date +import javax.inject.Inject + +internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit> + +internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( + private val capabilitiesAPI: CapabilitiesAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus, + private val getWellknownTask: GetWellknownTask, + private val configExtractor: IntegrationManagerConfigExtractor, + private val homeServerConnectionConfig: HomeServerConnectionConfig, + @UserId + private val userId: String +) : GetHomeServerCapabilitiesTask { + + override suspend fun execute(params: Unit) { + var doRequest = false + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time + } + + if (!doRequest) { + return + } + + val capabilities = runCatching { + executeRequest<GetCapabilitiesResult>(eventBus) { + apiCall = capabilitiesAPI.getCapabilities() + } + }.getOrNull() + + val uploadCapabilities = runCatching { + executeRequest<GetUploadCapabilitiesResult>(eventBus) { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + }.getOrNull() + + val versions = runCatching { + executeRequest<Versions>(null) { + apiCall = capabilitiesAPI.getVersions() + } + }.getOrNull() + + val wellknownResult = runCatching { + getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) + }.getOrNull() + + insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) + } + + private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, + getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getVersionResult: Versions?, + getWellknownResult: WellknownResult?) { + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + if (getCapabilitiesResult != null) { + homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + } + + if (getUploadCapabilitiesResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + } + + if (getVersionResult != null) { + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() + } + + if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { + homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl + homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true + // We are also checking for integration manager configurations + val config = configExtractor.extract(getWellknownResult.wellKnown) + if (config != null) { + Timber.v("Extracted integration config : $config") + realm.insertOrUpdate(config) + } + } else { + homeServerCapabilitiesEntity.adminE2EByDefault = true + } + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time + } + } + + companion object { + // 8 hours like on Riot Web + private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0989d6c1917935e75445f35ee1453df49ca72d8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import javax.inject.Inject + +internal class DefaultHomeServerCapabilitiesService @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : HomeServerCapabilitiesService { + + override fun getHomeServerCapabilities(): HomeServerCapabilities { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + } + } + ?: HomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..54d8cc7839c2357a26b879a0de521f8f7fa2da40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orTrue + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-capabilities + */ +@JsonClass(generateAdapter = true) +internal data class GetCapabilitiesResult( + /** + * Required. The custom capabilities the server supports, using the Java package naming convention. + */ + @Json(name = "capabilities") + val capabilities: Capabilities? = null +) + +@JsonClass(generateAdapter = true) +internal data class Capabilities( + /** + * Capability to indicate if the user can change their password. + */ + @Json(name = "m.change_password") + val changePassword: ChangePassword? = null + + // No need for m.room_versions for the moment +) + +@JsonClass(generateAdapter = true) +internal data class ChangePassword( + /** + * Required. True if the user can change their password, false otherwise. + */ + @Json(name = "enabled") + val enabled: Boolean? +) + +// The spec says: If not present, the client should assume that password changes are possible via the API +internal fun GetCapabilitiesResult.canChangePassword(): Boolean { + return capabilities?.changePassword?.enabled.orTrue() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..43395aae3e8d4d51d342b13966879b41626395a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GetUploadCapabilitiesResult( + /** + * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. + * If not listed or null, the size limit should be treated as unknown. + */ + @Json(name = "m.upload.size") + val maxUploadSize: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..240839cf7b89d926ae68d88617d9f47f6b3bbe17 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.wellknown.WellknownModule +import retrofit2.Retrofit + +@Module(includes = [WellknownModule::class]) +internal abstract class HomeServerCapabilitiesModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI { + return retrofit.create(CapabilitiesAPI::class.java) + } + } + + @Binds + abstract fun bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt new file mode 100644 index 0000000000000000000000000000000000000000..dee73f08f1e87756b89643cfce0c00126f454cf4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.TaskExecutor +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor, + private val capabilitiesAPI: CapabilitiesAPI) { + + fun canReachHomeServer(callback: (Boolean) -> Unit) { + taskExecutor.executorScope.launch { + val canReach = canReachHomeServer() + callback(canReach) + } + } + + suspend fun canReachHomeServer(): Boolean { + return try { + executeRequest<Unit>(null) { + apiCall = capabilitiesAPI.ping() + } + true + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError) { + (throwable.httpCode == 404 || throwable.httpCode == 400) + } else { + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3a10764d3c056ae9b10bebef36e9fc5448cbd2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityService +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.IdentityServiceListener +import org.matrix.android.sdk.api.session.identity.SharedState +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.extensions.observeNotNull +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask +import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.ensureProtocol +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@SessionScope +internal class DefaultIdentityService @Inject constructor( + private val identityStore: IdentityStore, + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityBulkLookupTask: IdentityBulkLookupTask, + private val identityRegisterTask: IdentityRegisterTask, + private val identityPingTask: IdentityPingTask, + private val identityDisconnectTask: IdentityDisconnectTask, + private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask, + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + @AuthenticatedIdentity + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val bindThreePidsTask: BindThreePidsTask, + private val submitTokenForBindingTask: IdentitySubmitTokenForBindingTask, + private val unbindThreePidsTask: UnbindThreePidsTask, + private val identityApiProvider: IdentityApiProvider, + private val accountDataDataSource: AccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val sessionParams: SessionParams, + private val taskExecutor: TaskExecutor +) : IdentityService, SessionLifecycleObserver { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = mutableSetOf<IdentityServiceListener>() + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + // Observe the account data change + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_IDENTITY_SERVER) + .observeNotNull(lifecycleOwner) { + notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel<IdentityServerContent>()?.baseUrl) + } + + // Init identityApi + updateIdentityAPI(identityStore.getIdentityData()?.identityServerUrl) + } + + private fun notifyIdentityServerUrlChange(baseUrl: String?) { + // This is maybe not a real change (echo of account data we are just setting) + if (identityStore.getIdentityData()?.identityServerUrl == baseUrl) { + Timber.d("Echo of local identity server url change, or no change") + } else { + // Url has changed, we have to reset our store, update internal configuration and notify listeners + identityStore.setUrl(baseUrl) + updateIdentityAPI(baseUrl) + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + } + + override fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + /** + * First return the identity server provided during login phase. + * If null, provide the one in wellknown configuration of the homeserver + * Else return null + */ + override fun getDefaultIdentityServer(): String? { + return sessionParams.defaultIdentityServerUrl + ?.takeIf { it.isNotEmpty() } + ?: homeServerCapabilitiesService.getHomeServerCapabilities().defaultIdentityServerUrl + } + + override fun getCurrentIdentityServerUrl(): String? { + return identityStore.getIdentityData()?.identityServerUrl + } + + override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) + } + } + + override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityStore.deletePendingBinding(threePid) + } + } + + override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) + } + } + + override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) + } + } + + override fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) + } + } + + override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) + } + } + + override fun isValidIdentityServer(url: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + identityPingTask.execute(IdentityPingTask.Params(api)) + } + } + + override fun disconnect(callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityDisconnectTask.execute(Unit) + + identityStore.setUrl(null) + updateIdentityAPI(null) + updateAccountData(null) + } + } + + override fun setNewIdentityServer(url: String, callback: MatrixCallback<String>): Cancelable { + val urlCandidate = url.ensureProtocol() + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val current = getCurrentIdentityServerUrl() + if (urlCandidate == current) { + // Nothing to do + Timber.d("Same URL, nothing to do") + } else { + // Disconnect previous one if any, first, because the token will change. + // In case of error when configuring the new identity server, this is not a big deal, + // we will ask for a new token on the previous Identity server + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + // Try to get a token + val token = getNewIdentityServerToken(urlCandidate) + + identityStore.setUrl(urlCandidate) + identityStore.setToken(token) + updateIdentityAPI(urlCandidate) + + updateAccountData(urlCandidate) + } + urlCandidate + } + } + + private suspend fun updateAccountData(url: String?) { + // Also notify the listener + withContext(coroutineDispatchers.main) { + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityServerContent(baseUrl = url) + )) + } + + override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyList()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + lookUpInternal(true, threePids) + } + } + + override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyMap()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val lookupResult = lookUpInternal(true, threePids) + + threePids.associateWith { threePid -> + // If not in lookup result, check if there is a pending binding + if (lookupResult.firstOrNull { it.threePid == threePid } == null) { + if (identityStore.getPendingBinding(threePid) == null) { + SharedState.NOT_SHARED + } else { + SharedState.BINDING_IN_PROGRESS + } + } else { + SharedState.SHARED + } + } + } + } + + private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> { + ensureIdentityTokenTask.execute(Unit) + + return try { + identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids)) + } catch (throwable: Throwable) { + // Refresh token? + when { + throwable.isInvalidToken() && canRetry -> { + identityStore.setToken(null) + lookUpInternal(false, threePids) + } + throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException + else -> throw throwable + } + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + override fun addListener(listener: IdentityServiceListener) { + listeners.add(listener) + } + + override fun removeListener(listener: IdentityServiceListener) { + listeners.remove(listener) + } + + private fun updateIdentityAPI(url: String?) { + identityApiProvider.identityApi = url + ?.let { retrofitFactory.create(okHttpClient, it) } + ?.create(IdentityAPI::class.java) + } +} + +private fun Throwable.isInvalidToken(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ +} + +private fun Throwable.isTermsNotSigned(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ + && error.code == MatrixError.M_TERMS_NOT_SIGNED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt new file mode 100644 index 0000000000000000000000000000000000000000..838b9975b7bfe77427766c10b1acf0a57e3e0b7e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import dagger.Lazy +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface EnsureIdentityTokenTask : Task<Unit, Unit> + +internal class DefaultEnsureIdentityTokenTask @Inject constructor( + private val identityStore: IdentityStore, + private val retrofitFactory: RetrofitFactory, + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask +) : EnsureIdentityTokenTask { + + override suspend fun execute(params: Unit) { + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured + + if (identityData.token == null) { + // Try to get a token + val token = getNewIdentityServerToken(url) + identityStore.setToken(token) + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..f2c889f024f0ccfa4f6e9f7d0dfe984c9e277536 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwnershipParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which need an identity server token + */ +internal interface IdentityAPI { + /** + * Gets information about what user owns the access token used in the request. + * Will return a 403 for when terms are not signed + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-account + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") + fun getAccount(): Call<IdentityAccountResponse> + + /** + * Logs out the access token, preventing it from being used to authenticate future requests to the server. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout") + fun logout(): Call<Unit> + + /** + * Request the hash detail to request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-hash-details + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") + fun hashDetails(): Call<IdentityHashDetailResponse> + + /** + * Request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-lookup + * + * @param body the body request + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") + fun lookup(@Body body: IdentityLookUpParams): Call<IdentityLookUpResponse> + + /** + * Create a session to change the bind status of an email to an identity server + * The identity server will also send an email + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken") + fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call<IdentityRequestTokenResponse> + + /** + * Create a session to change the bind status of an phone number to an identity server + * The identity server will also send an SMS on the ThreePid provided + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken") + fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call<IdentityRequestTokenResponse> + + /** + * Validate ownership of an email address, or a phone number. + * Ref: + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-msisdn-submittoken + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-email-submittoken + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") + fun submitToken(@Path("medium") medium: String, + @Body body: IdentityRequestOwnershipParams): Call<SuccessResult> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf9ba0ee8909f7ce01542a33f913bb8adbb96d0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import javax.inject.Inject + +internal class IdentityAccessTokenProvider @Inject constructor( + private val identityStore: IdentityStore +) : AccessTokenProvider { + override fun getToken() = identityStore.getIdentityData()?.token +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..09922cb4759a76847c92878f25cec6736b316c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ebe775ce5dd21da2027faab164914c8fa5a4c6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server exists and is alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2 + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) + fun ping(): Call<Unit> + + /** + * Ping v1 will be used to check outdated Identity server + */ + @GET("_matrix/identity/api/v1") + fun pingV1(): Call<Unit> + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call<IdentityRegisterResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac33c2666f7abbaf53a5908b1baa62b4c78dc393 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse +import org.matrix.android.sdk.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface IdentityBulkLookupTask : Task<IdentityBulkLookupTask.Params, List<FoundThreePid>> { + data class Params( + val threePids: List<ThreePid> + ) +} + +internal class DefaultIdentityBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityBulkLookupTask { + + override suspend fun execute(params: IdentityBulkLookupTask.Params): List<FoundThreePid> { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val pepper = identityData.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + fetchAndStoreHashDetails(identityAPI) + } else { + IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache could be outdated, the identity server maybe now supports sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true) + + // Convert back to List<FoundThreePid> + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private suspend fun lookUpInternal(identityAPI: IdentityAPI, + hashedAddresses: List<String>, + hashDetailResponse: IdentityHashDetailResponse, + canRetry: Boolean): IdentityLookUpResponse { + return try { + executeRequest(null) { + apiCall = identityAPI.lookup(IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + )) + } + } catch (failure: Throwable) { + // Catch invalid hash pepper and retry + if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { + // This is not documented, by the error can contain the new pepper! + if (!failure.error.newLookupPepper.isNullOrEmpty()) { + // Store it and use it right now + hashDetailResponse.copy(pepper = failure.error.newLookupPepper) + .also { identityStore.setHashDetails(it) } + .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) } + } else { + // Retrieve the new hash details + val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) + + if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + } + } else { + // Other error + throw failure + } + } + } + + private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest<IdentityHashDetailResponse>(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityStore.setHashDetails(it) } + } + + private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpResponse: IdentityLookUpResponse): List<FoundThreePid> { + return identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e68689ce7ff598df6a93670b3079fada9fa5bdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface IdentityDisconnectTask : Task<Unit, Unit> + +internal class DefaultIdentityDisconnectTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : IdentityDisconnectTask { + + override suspend fun execute(params: Unit) { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Ensure we have a token. + // We can have an identity server configured, but no token yet. + if (accessTokenProvider.getToken() == null) { + Timber.d("No token to disconnect identity server.") + return + } + + executeRequest<Unit>(null) { + apiCall = identityAPI.logout() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c35eef6423d1a3e0456bb9ac33e82811ae85921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.di.IdentityDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.SessionModule +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule +import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import java.io.File + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .build() + } + + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore + + @Binds + abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask + + @Binds + abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask + + @Binds + abstract fun bindIdentitySubmitTokenForBindingTask(task: DefaultIdentitySubmitTokenForBindingTask): IdentitySubmitTokenForBindingTask + + @Binds + abstract fun bindIdentityBulkLookupTask(task: DefaultIdentityBulkLookupTask): IdentityBulkLookupTask + + @Binds + abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..6994ef1bce94831bde6e3bc9556b1502a75a5667 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface IdentityPingTask : Task<IdentityPingTask.Params, Unit> { + data class Params( + val identityAuthAPI: IdentityAuthAPI + ) +} + +internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask { + + override suspend fun execute(params: IdentityPingTask.Params) { + try { + executeRequest<Unit>(null) { + apiCall = params.identityAuthAPI.ping() + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Check if API v1 is available + executeRequest<Unit>(null) { + apiCall = params.identityAuthAPI.pingV1() + } + // API V1 is responding, but not V2 -> Outdated + throw IdentityServiceError.OutdatedIdentityServer + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..bba6f991783f34dd5f51b8ba0bbaaa7f09e98534 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task<IdentityRegisterTask.Params, IdentityRegisterResponse> { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..3155e19943641ff655e643e9e6a777f577d96921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.getCountryCode +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse +import org.matrix.android.sdk.internal.task.Task +import java.util.UUID +import javax.inject.Inject + +internal interface IdentityRequestTokenForBindingTask : Task<IdentityRequestTokenForBindingTask.Params, Unit> { + data class Params( + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean + ) +} + +internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityRequestTokenForBindingTask { + + override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) + + if (params.sendAgain && identityPendingBinding == null) { + throw IdentityServiceError.NoCurrentBindingError + } + + val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() + val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 + + val tokenResponse = executeRequest<IdentityRequestTokenResponse>(null) { + apiCall = when (params.threePid) { + is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + )) + is ThreePid.Msisdn -> { + identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + )) + } + } + } + + // Store client secret, send attempt and sid + identityStore.storePendingBinding( + params.threePid, + IdentityPendingBinding( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = tokenResponse.sid + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b25986ae31434a3d2c683d7e3f8c5cf2268542b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwnershipParams +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface IdentitySubmitTokenForBindingTask : Task<IdentitySubmitTokenForBindingTask.Params, Unit> { + data class Params( + val threePid: ThreePid, + val token: String + ) +} + +internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentitySubmitTokenForBindingTask { + + override suspend fun execute(params: IdentitySubmitTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + val tokenResponse = executeRequest<SuccessResult>(null) { + apiCall = identityAPI.submitToken( + params.threePid.toMedium(), + IdentityRequestOwnershipParams( + clientSecret = identityPendingBinding.clientSecret, + sid = identityPendingBinding.sid, + token = params.token + )) + } + + if (!tokenResponse.isSuccess()) { + throw IdentityServiceError.BindingError + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..52f29c857be782a44345ff79e55bbc26b3f8df3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse + +internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Always check that we have access to the service (regarding terms) + val identityAccountResponse = executeRequest<IdentityAccountResponse>(null) { + apiCall = identityAPI.getAccount() + } + + assert(userId == identityAccountResponse.userId) + + return identityAPI +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt new file mode 100644 index 0000000000000000000000000000000000000000..4574d9d598f0489dbaafdd03d413c90c927375e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +internal data class IdentityData( + val identityServerUrl: String?, + val token: String?, + val hashLookupPepper: String?, + val hashLookupAlgorithm: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt new file mode 100644 index 0000000000000000000000000000000000000000..85bf65d741a88d77f0b69b654fc79b04318c1bcc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +internal data class IdentityPendingBinding( + /* Managed by Riot */ + val clientSecret: String, + /* Managed by Riot */ + val sendAttempt: Int, + /* Provided by the identity server */ + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..61334f188d78b3952a8d6f1e3715ef9d85a431c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityStore { + + fun getIdentityData(): IdentityData? + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) + + /** + * Store details about a current binding + */ + fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) + + fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? + + fun deletePendingBinding(threePid: ThreePid) +} + +internal fun IdentityStore.getIdentityServerUrlWithoutProtocol(): String? { + return getIdentityData()?.identityServerUrl?.substringAfter("://") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..41645bc07b2302cc58c32cf2df9648ad9625df79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityDataEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c612d6bf4cd9a698586e5257e7abda55c12174a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityDataEntity.Companion.get(realm: Realm): IdentityDataEntity? { + return realm.where<IdentityDataEntity>().findFirst() +} + +private fun IdentityDataEntity.Companion.getOrCreate(realm: Realm): IdentityDataEntity { + return get(realm) ?: realm.createObject() +} + +internal fun IdentityDataEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where<IdentityDataEntity>().findAll().deleteAllFromRealm() + // Delete all pending binding if any + IdentityPendingBindingEntity.deleteAll(realm) + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityDataEntity.Companion.setToken(realm: Realm, + newToken: String?) { + get(realm)?.apply { + token = newToken + } +} + +internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List<String>) { + get(realm)?.apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList<String>().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..4b99ba17d3727722c7c0fcde279e36daec441b07 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.internal.session.identity.data.IdentityData +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding + +internal object IdentityMapper { + + fun map(entity: IdentityDataEntity): IdentityData { + return IdentityData( + identityServerUrl = entity.identityServerUrl, + token = entity.token, + hashLookupPepper = entity.hashLookupPepper, + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + ) + } + + fun map(entity: IdentityPendingBindingEntity): IdentityPendingBinding { + return IdentityPendingBinding( + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..94a359f4d11a203fab73b2a129dc3b43aa777fa7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class IdentityPendingBindingEntity( + @PrimaryKey var threePid: String = "", + /* Managed by Riot */ + var clientSecret: String = "", + /* Managed by Riot */ + var sendAttempt: Int = 0, + /* Provided by the identity server */ + var sid: String = "" +) : RealmObject() { + + companion object { + fun ThreePid.toPrimaryKey() = "${toMedium()}_$value" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt new file mode 100644 index 0000000000000000000000000000000000000000..30de96e55735cd81189917b83e913c3d22a9cc0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { + return realm.where<IdentityPendingBindingEntity>() + .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) + .findFirst() +} + +internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity { + return get(realm, threePid) ?: realm.createObject(threePid.toPrimaryKey()) +} + +internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { + get(realm, threePid)?.deleteFromRealm() +} + +internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { + realm.where<IdentityPendingBindingEntity>() + .findAll() + .deleteAllFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..85ea903ab6e1a860f6c764cf04bab05f26e176a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityDataEntity::class, + IdentityPendingBindingEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..244f09f06a2a5ff099a41e6abcb35e9c7aeecd57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.di.IdentityDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding +import org.matrix.android.sdk.internal.session.identity.data.IdentityData +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +@SessionScope +internal class RealmIdentityStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityStore { + + override fun getIdentityData(): IdentityData? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityDataEntity.get(realm)?.let { IdentityMapper.map(it) } + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUrl(realm, url) + } + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setToken(realm, token) + } + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } + } + + override fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity -> + entity.clientSecret = data.clientSecret + entity.sendAttempt = data.sendAttempt + entity.sid = data.sid + } + } + } + } + + override fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityPendingBindingEntity.get(realm, threePid)?.let { IdentityMapper.map(it) } + } + } + + override fun deletePendingBinding(threePid: ThreePid) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.delete(realm, threePid) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..f23c4932060481aecb4584e72e872345ed1b8464 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + /** + * Required. The user ID which registered the token. + */ + @Json(name = "user_id") + val userId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..04ed62bdddf1e84a7fd197b62bc193744b070925 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + /** + * Required. The pepper the client MUST use in hashing identifiers, and MUST supply to the /lookup endpoint when performing lookups. + * Servers SHOULD rotate this string often. + */ + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * Required. The algorithms the server supports. Must contain at least "sha256". + * "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List<String> +) { + companion object { + const val ALGORITHM_SHA256 = "sha256" + const val ALGORITHM_NONE = "none" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..f737e4742c6cde8dec9548ef978edef6ba9dc75b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpParams( + /** + * Required. The addresses to look up. The format of the entries here depend on the algorithm used. + * Note that queries which have been incorrectly hashed or formatted will lead to no matches. + */ + @Json(name = "addresses") + val hashedAddresses: List<String>, + + /** + * Required. The algorithm the client is using to encode the addresses. This should be one of the available options from /hash_details. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * Required. The pepper from /hash_details. This is required even when the algorithm does not make use of it. + */ + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a274ebe44799077b5b50671257d8e83bb3b96ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpResponse( + /** + * Required. Any applicable mappings of addresses to Matrix User IDs. Addresses which do not have associations will + * not be included, which can make this property be an empty object. + */ + @Json(name = "mappings") + val mappings: Map<String, String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..1769b3654ea985e7e7371e7d5224666aaafed437 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * Required. An opaque string representing the token to authenticate future requests to the identity server with. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd263e1dc60f883f7afd84b15588fdc632b18c22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + /** + * Required. The client secret that was supplied to the requestToken call. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session ID, generated by the requestToken call. + */ + @Json(name = "sid") + val sid: String, + + /** + * Required. The token generated by the requestToken call and sent to the user. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..b93a3f43ae145d6dc87aee44f864128cd7c33cb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Just to consider common parameters +private interface IdentityRequestTokenBody { + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. + * Its length must not exceed 255 characters and it must not be empty. + */ + val clientSecret: String + + val sendAttempt: Int +} + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForEmailBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String +) : IdentityRequestTokenBody + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForMsisdnBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most recent one + * which it has seen, scoped to that country + phone_number + client_secret triple. This is to avoid repeatedly + * sending the same SMS in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new SMS (e.g. a reminder) to be sent. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone_number + * should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String +) : IdentityRequestTokenBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f4209cac81868b9a2b22be0541387c65ad11105 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenResponse( + /** + * Required. The session ID. Session IDs are opaque strings generated by the identity server. + * They must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ea6e4d4d7bd2b1a8ff6a4adf52128974e0660a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AllowedWidgetsContent( + /** + * Map of stateEventId to Allowed + */ + @Json(name = "widgets") val widgets: Map<String, Boolean> = emptyMap(), + + /** + * Map of native widgetType to a map of domain to Allowed + * { + * "jitsi" : { + * "jitsi.domain.org" : true, + * "jitsi.other.org" : false + * } + * } + */ + @Json(name = "native_widgets") val native: Map<String, Map<String, Boolean>> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt new file mode 100644 index 0000000000000000000000000000000000000000..4f1185929fadac2e605c508fcb186d56788d6b78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.util.Cancelable +import javax.inject.Inject + +internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService { + + override fun addListener(listener: IntegrationManagerService.Listener) { + integrationManager.addListener(listener) + } + + override fun removeListener(listener: IntegrationManagerService.Listener) { + integrationManager.removeListener(listener) + } + + override fun getOrderedConfigs(): List<IntegrationManagerConfig> { + return integrationManager.getOrderedConfigs() + } + + override fun getPreferredConfig(): IntegrationManagerConfig { + return integrationManager.getPreferredConfig() + } + + override fun isIntegrationEnabled(): Boolean { + return integrationManager.isIntegrationEnabled() + } + + override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable { + return integrationManager.setIntegrationEnabled(enable, callback) + } + + override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { + return integrationManager.setWidgetAllowed(stateEventId, allowed, callback) + } + + override fun isWidgetAllowed(stateEventId: String): Boolean { + return integrationManager.isWidgetAllowed(stateEventId) + } + + override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { + return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback) + } + + override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean { + return integrationManager.isNativeWidgetDomainAllowed(widgetType, domain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f7b3f85c7940d05ffbd30072c34cbb30e588998 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.extensions.observeNotNull +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory +import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +/** + * The integration manager allows to + * - Get the Integration Manager that a user has explicitly set for its account (via account data) + * - Get the recommended/preferred Integration Manager list as defined by the HomeServer (via wellknown) + * - Check if the user has disabled the integration manager feature + * - Allow / Disallow Integration manager (propagated to other riot clients) + * + * The integration manager listen to account data, and can notify observer for changes. + * + * The wellknown is refreshed at each application fresh start + * + */ +@SessionScope +internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val accountDataDataSource: AccountDataDataSource, + private val widgetFactory: WidgetFactory) + : SessionLifecycleObserver { + + private val currentConfigs = ArrayList<IntegrationManagerConfig>() + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = HashSet<IntegrationManagerService.Listener>() + fun addListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.add(listener) } + fun removeListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.remove(listener) } + + init { + val defaultConfig = IntegrationManagerConfig( + uiUrl = matrixConfiguration.integrationUIUrl, + restUrl = matrixConfiguration.integrationRestUrl, + kind = IntegrationManagerConfig.Kind.DEFAULT + ) + currentConfigs.add(defaultConfig) + } + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + observeWellknownConfig() + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + .observeNotNull(lifecycleOwner) { + val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>() + if (allowedWidgetsContent != null) { + notifyWidgetPermissionsChanged(allowedWidgetsContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING) + .observeNotNull(lifecycleOwner) { + val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>() + if (integrationProvisioningContent != null) { + notifyIsEnabledChanged(integrationProvisioningContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) + .observeNotNull(lifecycleOwner) { + val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent() + val config = integrationManagerContent?.extractIntegrationManagerConfig() + updateCurrentConfigs(IntegrationManagerConfig.Kind.ACCOUNT, config) + } + } + + override fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun hasConfig() = currentConfigs.isNotEmpty() + + fun getOrderedConfigs(): List<IntegrationManagerConfig> { + return currentConfigs.sortedBy { + it.kind + } + } + + fun getPreferredConfig(): IntegrationManagerConfig { + // This can't be null as we should have at least the default one registered + return getOrderedConfigs().first() + } + + /** + * Returns false if the user as disabled integration manager feature + */ + fun isIntegrationEnabled(): Boolean { + val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING) + val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>() + return integrationProvisioningContent?.enabled ?: false + } + + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable { + val isIntegrationEnabled = isIntegrationEnabled() + if (enable == isIntegrationEnabled) { + callback.onSuccess(Unit) + return NoOpCancellable + } + val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable) + val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() + val newContent = if (currentContent == null) { + val allowedWidget = mapOf(stateEventId to allowed) + AllowedWidgetsContent(widgets = allowedWidget, native = emptyMap()) + } else { + val allowedWidgets = currentContent.widgets.toMutableMap().apply { + put(stateEventId, allowed) + } + currentContent.copy(widgets = allowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isWidgetAllowed(stateEventId: String): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() + return currentContent?.widgets?.get(stateEventId) ?: false + } + + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() + val newContent = if (currentContent == null) { + val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) + AllowedWidgetsContent(widgets = emptyMap(), native = nativeAllowedWidgets) + } else { + val nativeAllowedWidgets = currentContent.native.toMutableMap().apply { + (get(widgetType))?.let { + set(widgetType, it.toMutableMap().apply { set(domain, allowed) }) + } ?: run { + set(widgetType, mapOf(domain to allowed)) + } + } + currentContent.copy(native = nativeAllowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() + return currentContent?.native?.get(widgetType)?.get(domain) ?: false + } + + private fun notifyConfigurationChanged() { + synchronized(listeners) { + listeners.forEach { + try { + it.onConfigurationChanged(currentConfigs) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyWidgetPermissionsChanged(allowedWidgets: AllowedWidgetsContent) { + Timber.v("On widget permissions changed: $allowedWidgets") + synchronized(listeners) { + listeners.forEach { + try { + it.onWidgetPermissionsChanged(allowedWidgets.widgets) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyIsEnabledChanged(provisioningContent: IntegrationProvisioningContent) { + Timber.v("On provisioningContent changed : $provisioningContent") + synchronized(listeners) { + listeners.forEach { + try { + it.onIsEnabledChanged(provisioningContent.enabled) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun WidgetContent.extractIntegrationManagerConfig(): IntegrationManagerConfig? { + if (url.isNullOrBlank()) { + return null + } + val integrationManagerData = data.toModel<IntegrationManagerWidgetData>() + return IntegrationManagerConfig( + uiUrl = url, + restUrl = integrationManagerData?.apiUrl ?: url, + kind = IntegrationManagerConfig.Kind.ACCOUNT + ) + } + + private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { + return extractWidgetSequence(widgetFactory) + .filter { + WidgetType.IntegrationManager == it.type + } + .firstOrNull()?.widgetContent + } + + private fun observeWellknownConfig() { + val liveData = monarchy.findAllMappedWithChanges( + { it.where(WellknownIntegrationManagerConfigEntity::class.java) }, + { IntegrationManagerConfig(it.uiUrl, it.apiUrl, IntegrationManagerConfig.Kind.HOMESERVER) } + ) + liveData.observeNotNull(lifecycleOwner) { + val config = it.firstOrNull() + updateCurrentConfigs(IntegrationManagerConfig.Kind.HOMESERVER, config) + } + } + + private fun updateCurrentConfigs(kind: IntegrationManagerConfig.Kind, config: IntegrationManagerConfig?) { + val hasBeenRemoved = currentConfigs.removeAll { currentConfig -> + currentConfig.kind == kind + } + if (config != null) { + currentConfigs.add(config) + } + if (hasBeenRemoved || config != null) { + notifyConfigurationChanged() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..78edc59fcaedc53276fd2de263ed6b277a15ee41 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import org.matrix.android.sdk.api.auth.data.WellKnown +import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity +import javax.inject.Inject + +internal class IntegrationManagerConfigExtractor @Inject constructor() { + + fun extract(wellKnown: WellKnown): WellknownIntegrationManagerConfigEntity? { + wellKnown.integrations?.get("managers")?.let { + (it as? List<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + return WellknownIntegrationManagerConfigEntity( + apiUrl = apiUrl, + uiUrl = uiUrl + ) + } + } + } + } + } + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..fb7f835d9b47a5216876a795a0df70ad6de3339e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService + +@Module +internal abstract class IntegrationManagerModule { + + @Binds + abstract fun bindIntegrationManagerService(service: DefaultIntegrationManagerService): IntegrationManagerService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt new file mode 100644 index 0000000000000000000000000000000000000000..c592237a1f1e5aa9920b1a39a8c3c3f17c91af3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationManagerWidgetData( + @Json(name = "api_url") val apiUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..e48a6fd84f623b70a14102266d54798122819d39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationProvisioningContent( + @Json(name = "enabled") val enabled: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ffe11ee04ba1e281660907ece184a188a7d014f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.notification + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.rest.RuleSet +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.pushers.AddPushRuleTask +import org.matrix.android.sdk.internal.session.pushers.GetPushRulesTask +import org.matrix.android.sdk.internal.session.pushers.RemovePushRuleTask +import org.matrix.android.sdk.internal.session.pushers.UpdatePushRuleActionsTask +import org.matrix.android.sdk.internal.session.pushers.UpdatePushRuleEnableStatusTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultPushRuleService @Inject constructor( + private val getPushRulesTask: GetPushRulesTask, + private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, + private val addPushRuleTask: AddPushRuleTask, + private val updatePushRuleActionsTask: UpdatePushRuleActionsTask, + private val removePushRuleTask: RemovePushRuleTask, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy +) : PushRuleService { + + private var listeners = mutableSetOf<PushRuleService.PushRuleListener>() + + override fun fetchPushRules(scope: String) { + getPushRulesTask + .configureWith(GetPushRulesTask.Params(scope)) + .executeBy(taskExecutor) + } + + override fun getPushRules(scope: String): RuleSet { + var contentRules: List<PushRule> = emptyList() + var overrideRules: List<PushRule> = emptyList() + var roomRules: List<PushRule> = emptyList() + var senderRules: List<PushRule> = emptyList() + var underrideRules: List<PushRule> = emptyList() + + monarchy.doWithRealm { realm -> + PushRulesEntity.where(realm, scope, RuleSetKey.CONTENT) + .findFirst() + ?.let { pushRulesEntity -> + contentRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapContentRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.OVERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + overrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.ROOM) + .findFirst() + ?.let { pushRulesEntity -> + roomRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapRoomRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.SENDER) + .findFirst() + ?.let { pushRulesEntity -> + senderRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapSenderRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.UNDERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + underrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } + } + + return RuleSet( + content = contentRules, + override = overrideRules, + room = roomRules, + sender = senderRules, + underride = underrideRules + ) + } + + override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable { + // The rules will be updated, and will come back from the next sync response + return updatePushRuleEnableStatusTask + .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { + return addPushRuleTask + .configureWith(AddPushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { + return updatePushRuleActionsTask + .configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { + return removePushRuleTask + .configureWith(RemovePushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } + + override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + +// fun processEvents(events: List<Event>) { +// var hasDoneSomething = false +// events.forEach { event -> +// fulfilledBingRule(event)?.let { +// hasDoneSomething = true +// dispatchBing(event, it) +// } +// } +// if (hasDoneSomething) +// dispatchFinish() +// } + + fun dispatchBing(event: Event, rule: PushRule) { + synchronized(listeners) { + val actionsList = rule.getActions() + listeners.forEach { + try { + it.onMatchRule(event, actionsList) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching bing") + } + } + } + } + + fun dispatchRoomJoined(roomId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onRoomJoined(roomId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room joined") + } + } + } + } + + fun dispatchRoomLeft(roomId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onRoomLeft(roomId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + } + } + + fun dispatchRedactedEventId(redactedEventId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onEventRedacted(redactedEventId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching redacted event") + } + } + } + } + + fun dispatchFinish() { + synchronized(listeners) { + listeners.forEach { + try { + it.batchFinish() + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching finish") + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..49a92acc5416c4dad7229b43c6ed25f75b4543d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.notification + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params, Unit> { + data class Params( + val syncResponse: RoomsSyncResponse, + val rules: List<PushRule> + ) +} + +internal class DefaultProcessEventForPushTask @Inject constructor( + private val defaultPushRuleService: DefaultPushRuleService, + private val conditionResolver: ConditionResolver, + @UserId private val userId: String +) : ProcessEventForPushTask { + + override suspend fun execute(params: ProcessEventForPushTask.Params) { + // Handle left rooms + params.syncResponse.leave.keys.forEach { + defaultPushRuleService.dispatchRoomLeft(it) + } + // Handle joined rooms + params.syncResponse.join.keys.forEach { + defaultPushRuleService.dispatchRoomJoined(it) + } + val newJoinEvents = params.syncResponse.join + .mapNotNull { (key, value) -> + value.timeline?.events?.map { it.copy(roomId = key) } + } + .flatten() + val inviteEvents = params.syncResponse.invite + .mapNotNull { (key, value) -> + value.inviteState?.events?.map { it.copy(roomId = key) } + } + .flatten() + val allEvents = (newJoinEvents + inviteEvents).filter { event -> + when (event.type) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.ENCRYPTED, + EventType.STATE_ROOM_MEMBER -> true + else -> false + } + }.filter { + it.senderId != userId + } + Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + + " to check for push rules with ${params.rules.size} rules") + allEvents.forEach { event -> + fulfilledBingRule(event, params.rules)?.let { + Timber.v("[PushRules] Rule $it match for event ${event.eventId}") + defaultPushRuleService.dispatchBing(event, it) + } + } + + val allRedactedEvents = params.syncResponse.join + .asSequence() + .mapNotNull { (_, value) -> value.timeline?.events } + .flatten() + .filter { it.type == EventType.REDACTION } + .mapNotNull { it.redacts } + .toList() + + Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") + + allRedactedEvents.forEach { redactedEventId -> + defaultPushRuleService.dispatchRedactedEventId(redactedEventId) + } + + defaultPushRuleService.dispatchFinish() + } + + private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? { + return rules.firstOrNull { rule -> + // All conditions must hold true for an event in order to apply the action for the event. + rule.enabled && rule.conditions?.all { + it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false + } ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..3da6fdca9397f78f56e1b711ccadcad51029b614 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetOpenIdTokenTask : Task<Unit, RequestOpenIdTokenResponse> + +internal class DefaultGetOpenIdTokenTask @Inject constructor( + @UserId private val userId: String, + private val openIdAPI: OpenIdAPI, + private val eventBus: EventBus) : GetOpenIdTokenTask { + + override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + return executeRequest(eventBus) { + apiCall = openIdAPI.openIdToken(userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..e56e2e630e993d04691a803240c273346a042613 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface OpenIdAPI { + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + * + * @param userId the user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, + @Body body: JsonDict = emptyMap()): Call<RequestOpenIdTokenResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..60ee7fb747736f1105bc2f32ad47f49093c394f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class OpenIdModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesOpenIdAPI(retrofit: Retrofit): OpenIdAPI { + return retrofit.create(OpenIdAPI::class.java) + } + } + + @Binds + abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..8103efb895a16bae4a98b7eae017f13f9ffaa47e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RequestOpenIdTokenResponse( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val openIdToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..185294fd3baaadbaadb098854241175a6f296400 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the ThreePids response + */ +@JsonClass(generateAdapter = true) +internal data class AccountThreePidsResponse( + @Json(name = "threepids") + val threePids: List<ThirdPartyIdentifier>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..dff246e6f1b2a75667dad2b7ad4cba212c594be3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BindThreePidBody( + /** + * Required. The client secret used in the session with the identity server. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The identity server to use. (without "https://") + */ + @Json(name = "id_server") + var identityServerUrlWithoutProtocol: String, + + /** + * Required. An access token previously registered with the identity server. + */ + @Json(name = "id_access_token") + var identityServerAccessToken: String, + + /** + * Required. The session identifier given by the identity server. + */ + @Json(name = "sid") + var sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..52fbcb518550e5f771d98f7820bb1e192321d181 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class BindThreePidsTask : Task<BindThreePidsTask.Params, Unit> { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultBindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider, + private val eventBus: EventBus) : BindThreePidsTask() { + override suspend fun execute(params: Params) { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + executeRequest<Unit>(eventBus) { + apiCall = profileAPI.bindThreePid( + BindThreePidBody( + clientSecret = identityPendingBinding.clientSecret, + identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, + identityServerAccessToken = identityServerAccessToken, + sid = identityPendingBinding.sid + )) + } + + // Binding is over, cleanup the store + identityStore.deletePendingBinding(params.threePid) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt new file mode 100644 index 0000000000000000000000000000000000000000..06dcaeccefca61e7503d442a90545acf9fbc8d16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.database.model.UserThreePidEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.content.FileUploader +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import io.realm.kotlin.where +import javax.inject.Inject + +internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val refreshUserThreePidsTask: RefreshUserThreePidsTask, + private val getProfileInfoTask: GetProfileInfoTask, + private val setDisplayNameTask: SetDisplayNameTask, + private val setAvatarUrlTask: SetAvatarUrlTask, + private val fileUploader: FileUploader) : ProfileService { + + override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback<JsonDict> { + override fun onSuccess(data: JsonDict) { + val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String + matrixCallback.onSuccess(Optional.from(displayName)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable { + return setDisplayNameTask + .configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { + val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") + setAvatarUrlTask + .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + } + + override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback<JsonDict> { + override fun onSuccess(data: JsonDict) { + val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String + matrixCallback.onSuccess(Optional.from(avatarUrl)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun getThreePids(): List<ThreePid> { + return monarchy.fetchAllMappedSync( + { it.where<UserThreePidEntity>() }, + { it.asDomain() } + ) + } + + override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> { + if (refreshData) { + // Force a refresh of the values + refreshUserThreePidsTask + .configureWith() + .executeBy(taskExecutor) + } + + return monarchy.findAllMappedWithChanges( + { it.where<UserThreePidEntity>() }, + { it.asDomain() } + ) + } +} + +private fun UserThreePidEntity.asDomain(): ThreePid { + return when (medium) { + ThirdPartyIdentifier.MEDIUM_EMAIL -> ThreePid.Email(address) + ThirdPartyIdentifier.MEDIUM_MSISDN -> ThreePid.Msisdn(address) + else -> error("Invalid medium type") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..7889dbf24058d3c3625cdc0475c18eecc300e95e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class GetProfileInfoTask : Task<GetProfileInfoTask.Params, JsonDict> { + data class Params( + val userId: String + ) +} + +internal class DefaultGetProfileInfoTask @Inject constructor(private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : GetProfileInfoTask() { + + override suspend fun execute(params: Params): JsonDict { + return executeRequest(eventBus) { + apiCall = profileAPI.getProfile(params.userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..31e1f09bbd79939646e93c9718ea7c74a05f932c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface ProfileAPI { + + /** + * Get the combined profile information for this user. + * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. + * This API may return keys which are not limited to displayname or avatar_url. + * @param userId the user id to fetch profile info + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") + fun getProfile(@Path("userId") userId: String): Call<JsonDict> + + /** + * List all 3PIDs linked to the Matrix user account. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") + fun getThreePIDs(): Call<AccountThreePidsResponse> + + /** + * Change user display name + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") + fun setDisplayName(@Path("userId") userId: String, + @Body body: SetDisplayNameBody): Call<Unit> + + /** + * Change user avatar url. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") + fun setAvatarUrl(@Path("userId") userId: String, + @Body body: SetAvatarUrlBody): Call<Unit> + + /** + * Bind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind") + fun bindThreePid(@Body body: BindThreePidBody): Call<Unit> + + /** + * Unbind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") + fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..57a86d03e08fcc50378219be686c0400143e6867 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class ProfileModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesProfileAPI(retrofit: Retrofit): ProfileAPI { + return retrofit.create(ProfileAPI::class.java) + } + } + + @Binds + abstract fun bindProfileService(service: DefaultProfileService): ProfileService + + @Binds + abstract fun bindGetProfileTask(task: DefaultGetProfileInfoTask): GetProfileInfoTask + + @Binds + abstract fun bindRefreshUserThreePidsTask(task: DefaultRefreshUserThreePidsTask): RefreshUserThreePidsTask + + @Binds + abstract fun bindBindThreePidsTask(task: DefaultBindThreePidsTask): BindThreePidsTask + + @Binds + abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask + + @Binds + abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask + + @Binds + abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..dcc0db8ad1c24824f5a01dd916102366d9f75b88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.profile + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.UserThreePidEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal abstract class RefreshUserThreePidsTask : Task<Unit, Unit> + +internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus) : RefreshUserThreePidsTask() { + + override suspend fun execute(params: Unit) { + val accountThreePidsResponse = executeRequest<AccountThreePidsResponse>(eventBus) { + apiCall = profileAPI.getThreePIDs() + } + + Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") + // Store the list in DB + monarchy.writeAsync { realm -> + realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() + accountThreePidsResponse.threePids?.forEach { + val entity = UserThreePidEntity( + it.medium?.takeIf { med -> med in ThirdPartyIdentifier.SUPPORTED_MEDIUM } ?: return@forEach, + it.address ?: return@forEach, + it.validatedAt.toLong(), + it.addedAt.toLong()) + realm.insertOrUpdate(entity) + } + } + } +} + +private fun Any?.toLong(): Long { + return when (this) { + null -> 0L + is Long -> this + is Double -> this.toLong() + else -> 0L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..25d995fbdf04a8c91eaebe2c968e012de42a3b97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SetAvatarUrlBody( + /** + * The new avatar url for this user. + */ + @Json(name = "avatar_url") + val avatarUrl: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..1eaedb0220de9762695286980db037c3d9e1a58f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class SetAvatarUrlTask : Task<SetAvatarUrlTask.Params, Unit> { + data class Params( + val userId: String, + val newAvatarUrl: String + ) +} + +internal class DefaultSetAvatarUrlTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : SetAvatarUrlTask() { + + override suspend fun execute(params: Params) { + return executeRequest(eventBus) { + val body = SetAvatarUrlBody( + avatarUrl = params.newAvatarUrl + ) + apiCall = profileAPI.setAvatarUrl(params.userId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..306aca6f445fab79b0a5170de93ca052c0aea9ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SetDisplayNameBody( + /** + * The new display name for this user. + */ + @Json(name = "displayname") + val displayName: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..66406a480c5b8c2186987b301439b19cc46e1729 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class SetDisplayNameTask : Task<SetDisplayNameTask.Params, Unit> { + data class Params( + val userId: String, + val newDisplayName: String + ) +} + +internal class DefaultSetDisplayNameTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : SetDisplayNameTask() { + + override suspend fun execute(params: Params) { + return executeRequest(eventBus) { + val body = SetDisplayNameBody( + displayName = params.newDisplayName + ) + apiCall = profileAPI.setDisplayName(params.userId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt new file mode 100755 index 0000000000000000000000000000000000000000..b7c756cbb70854921fb303e31ef74e8ab3116e2b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThirdPartyIdentifier( + /** + * Required. The medium of the third party identifier. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String? = null, + + /** + * Required. The third party identifier address. + */ + @Json(name = "address") + val address: String? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been validated. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "validated_at") + val validatedAt: Any? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been added to the user account. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "added_at") + val addedAt: Any? = null +) { + companion object { + const val MEDIUM_EMAIL = "email" + const val MEDIUM_MSISDN = "msisdn" + + val SUPPORTED_MEDIUM = listOf(MEDIUM_EMAIL, MEDIUM_MSISDN) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a91245894ae3704359880f5b600672df33049d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidBody( + /** + * The identity server to unbind from. If not provided, the homeserver MUST use the id_server the identifier was added through. + * If the homeserver does not know the original id_server, it MUST return a id_server_unbind_result of no-support. + */ + @Json(name = "id_server") + val identityServerUrlWithoutProtocol: String?, + + /** + * Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String, + + /** + * Required. The third party address being removed. + */ + @Json(name = "address") + val address: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..df31efdb6ccf58dd59227bc9bfe6f2743de983d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidResponse( + @Json(name = "id_server_unbind_result") + val idServerUnbindResult: String? +) { + fun isSuccess() = idServerUnbindResult == "success" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b08c283765364933228f76904fe8e9bf9cbe6567 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class UnbindThreePidsTask : Task<UnbindThreePidsTask.Params, Boolean> { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultUnbindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + private val eventBus: EventBus) : UnbindThreePidsTask() { + override suspend fun execute(params: Params): Boolean { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest<UnbindThreePidResponse>(eventBus) { + apiCall = profileAPI.unbindThreePid( + UnbindThreePidBody( + identityServerUrlWithoutProtocol, + params.threePid.toMedium(), + params.threePid.value + )) + }.isSuccess() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7f1fb2b9396ab05db649286802602e5638b0753 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val pusher: JsonPusher, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var pushersAPI: PushersAPI + @Inject @SessionDatabase lateinit var monarchy: Monarchy + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val pusher = params.pusher + + if (pusher.pushKey.isBlank()) { + return Result.failure() + } + return try { + setPusher(pusher) + Result.success() + } catch (exception: Throwable) { + when (exception) { + is Failure.NetworkConnection -> Result.retry() + else -> { + monarchy.awaitTransaction { realm -> + PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { + // update it + it.state = PusherState.FAILED_TO_REGISTER + } + } + Result.failure() + } + } + } + } + + private suspend fun setPusher(pusher: JsonPusher) { + executeRequest<Unit>(eventBus) { + apiCall = pushersAPI.setPusher(pusher) + } + monarchy.awaitTransaction { realm -> + val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() + if (echo != null) { + // update it + echo.appDisplayName = pusher.appDisplayName + echo.appId = pusher.appId + echo.kind = pusher.kind + echo.lang = pusher.lang + echo.profileTag = pusher.profileTag + echo.data?.format = pusher.data?.format + echo.data?.url = pusher.data?.url + echo.state = PusherState.REGISTERED + } else { + pusher.toEntity().also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c8f11a12dc7781a0266236c009ee5368c3d6d91 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddPushRuleTask : Task<AddPushRuleTask.Params, Unit> { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultAddPushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : AddPushRuleTask { + + override suspend fun execute(params: AddPushRuleTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d3ad340f55ed7e7b0b028ebc944c56c257c9ee3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition +import org.matrix.android.sdk.api.pushrules.EventMatchCondition +import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition +import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter +import javax.inject.Inject + +internal class DefaultConditionResolver @Inject constructor( + private val roomGetter: RoomGetter, + @UserId private val userId: String +) : ConditionResolver { + + override fun resolveEventMatchCondition(event: Event, + condition: EventMatchCondition): Boolean { + return condition.isSatisfied(event) + } + + override fun resolveRoomMemberCountCondition(event: Event, + condition: RoomMemberCountCondition): Boolean { + return condition.isSatisfied(event, roomGetter) + } + + override fun resolveSenderNotificationPermissionCondition(event: Event, + condition: SenderNotificationPermissionCondition): Boolean { + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + + val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel<PowerLevelsContent>() + ?: PowerLevelsContent() + + return condition.isSatisfied(event, powerLevelsContent) + } + + override fun resolveContainsDisplayNameCondition(event: Event, + condition: ContainsDisplayNameCondition): Boolean { + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false + return condition.isSatisfied(event, myDisplayName) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ef46785b2549fc3baba0f81ff00a4b80e1c0833 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import java.security.InvalidParameterException +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class DefaultPushersService @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + @SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val getPusherTask: GetPushersTask, + private val removePusherTask: RemovePusherTask, + private val taskExecutor: TaskExecutor +) : PushersService { + + override fun refreshPushers() { + getPusherTask + .configureWith() + .executeBy(taskExecutor) + } + + override fun addHttpPusher(pushkey: String, + appId: String, + profileTag: String, + lang: String, + appDisplayName: String, + deviceDisplayName: String, + url: String, + append: Boolean, + withEventIdOnly: Boolean) + : UUID { + // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem + if (pushkey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") + if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") + if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") + + val pusher = JsonPusher( + pushKey = pushkey, + kind = "http", + appId = appId, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + profileTag = profileTag, + lang = lang, + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), + append = append) + + val params = AddHttpPusherWorker.Params(sessionId, pusher) + + val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddHttpPusherWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(WorkerParamsFactory.toData(params)) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) + .build() + workManagerProvider.workManager.enqueue(request) + return request.id + } + + override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>): Cancelable { + val params = RemovePusherTask.Params(pushkey, appId) + return removePusherTask + .configureWith(params) { + this.callback = callback + } + // .enableRetry() ?? + .executeBy(taskExecutor) + } + + override fun getPushersLive(): LiveData<List<Pusher>> { + return monarchy.findAllMappedWithChanges( + { realm -> PusherEntity.where(realm) }, + { it.asDomain() } + ) + } + + override fun getPushers(): List<Pusher> { + return monarchy.fetchAllCopiedSync { PusherEntity.where(it) }.map { it.asDomain() } + } + + companion object { + const val EVENT_ID_ONLY = "event_id_only" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..de96db01ddac33a891ed0cfa4a208afe940fc688 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPushRulesTask : Task<GetPushRulesTask.Params, Unit> { + data class Params(val scope: String) +} + +/** + * We keep this task, but it should not be used anymore, the push rules comes from the sync response + */ +internal class DefaultGetPushRulesTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val savePushRulesTask: SavePushRulesTask, + private val eventBus: EventBus +) : GetPushRulesTask { + + override suspend fun execute(params: GetPushRulesTask.Params) { + val response = executeRequest<GetPushRulesResponse>(eventBus) { + apiCall = pushRulesApi.getAllRules() + } + + savePushRulesTask.execute(SavePushRulesTask.Params(response)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..a36705cc571cd41f5daf81c774818dd76dcb6d6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal class GetPushersResponse( + @Json(name = "pushers") + val pushers: List<JsonPusher>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..bad507555d4856583783dd49e8c8855814af66a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPushersTask : Task<Unit, Unit> + +internal class DefaultGetPushersTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : GetPushersTask { + + override suspend fun execute(params: Unit) { + val response = executeRequest<GetPushersResponse>(eventBus) { + apiCall = pushersAPI.getPushers() + } + monarchy.awaitTransaction { realm -> + // clear existings? + realm.where(PusherEntity::class.java) + .findAll().deleteAllFromRealm() + response.pushers?.forEach { jsonPusher -> + jsonPusher.toEntity().also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt new file mode 100644 index 0000000000000000000000000000000000000000..89dae0c7e9d5d16d083d20b6c90e31cdda923f63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.SerializeNulls + +/** + * Example: + * + * <code> + * { + * "pushers": [ + * { + * "pushkey": "Xp/MzCt8/9DcSNE9cuiaoT5Ac55job3TdLSSmtmYl4A=", + * "kind": "http", + * "app_id": "face.mcapp.appy.prod", + * "app_display_name": "Appy McAppface", + * "device_display_name": "Alice's Phone", + * "profile_tag": "xyz", + * "lang": "en-US", + * "data": { + * "url": "https://example.com/_matrix/push/v1/notify" + * } + * }] + * } + * </code> + */ +@JsonClass(generateAdapter = true) +internal data class JsonPusher( + /** + * Required. This is a unique identifier for this pusher. The value you should use for this is the routing or + * destination address information for the notification, for example, the APNS token for APNS or the + * Registration ID for GCM. If your notification client has no such concept, use any unique identifier. + * Max length, 512 bytes. + * + * If the kind is "email", this is the email address to send notifications to. + */ + @Json(name = "pushkey") + val pushKey: String, + + /** + * Required. The kind of pusher to configure. + * "http" makes a pusher that sends HTTP pokes. + * "email" makes a pusher that emails the user with unread notifications. + * null deletes the pusher. + */ + @SerializeNulls + @Json(name = "kind") + val kind: String?, + + /** + * Required. This is a reverse-DNS style identifier for the application. It is recommended that this end + * with the platform, such that different platform versions get different app identifiers. + * Max length, 64 chars. + * + * If the kind is "email", this is "m.email". + */ + @Json(name = "app_id") + val appId: String, + + /** + * Required. A string that will allow the user to identify what application owns this pusher. + */ + @Json(name = "app_display_name") + val appDisplayName: String? = null, + + /** + * Required. A string that will allow the user to identify what device owns this pusher. + */ + @Json(name = "device_display_name") + val deviceDisplayName: String? = null, + + /** + * This string determines which set of device specific rules this pusher executes. + */ + @Json(name = "profile_tag") + val profileTag: String? = null, + + /** + * Required. The preferred language for receiving notifications (e.g. 'en' or 'en-US') + */ + @Json(name = "lang") + val lang: String? = null, + + /** + * Required. A dictionary of information for the pusher implementation itself. + * If kind is http, this should contain url which is the URL to use to send notifications to. + */ + @Json(name = "data") + val data: JsonPusherData? = null, + + /** + * If true, the homeserver should add another pusher with the given pushkey and App ID in addition to any others + * with different user IDs. Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey + * for different users. + * The default is false. + */ + @Json(name = "append") + val append: Boolean? = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2520a915bf3f40d942740356f3f52c4fa6ea5b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class JsonPusherData( + /** + * Required if kind is http. The URL to use to send notifications to. + * MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + */ + @Json(name = "url") + val url: String? = null, + + /** + * The format to send notifications in to Push Gateways if the kind is http. + * Currently the only format available is 'event_id_only'. + */ + @Json(name = "format") + val format: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..166e8ac3bea2cfdd1d43f272bd18dac3e9fbeeb9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface PushRulesApi { + /** + * Get all push rules + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/") + fun getAllRules(): Call<GetPushRulesResponse> + + /** + * Update the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param enable the new enable status + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled") + fun updateEnableRuleStatus(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body enable: Boolean?) + : Call<Unit> + + /** + * Update the ruleID action + * Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param actions the actions + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions") + fun updateRuleActions(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body actions: Any) + : Call<Unit> + + /** + * Delete a rule + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") + fun deleteRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String) + : Call<Unit> + + /** + * Add the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId. + * @param rule the rule to add. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") + fun addRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body rule: PushRule) + : Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ad70db5e46b29e1761c21f1b4460a313a41f5c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +internal interface PushersAPI { + + /** + * Get the pushers for this user. + * + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers") + fun getPushers(): Call<GetPushersResponse> + + /** + * This endpoint allows the creation, modification and deletion of pushers for this user ID. + * The behaviour of this endpoint varies depending on the values in the JSON body. + * + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set") + fun setPusher(@Body jsonPusher: JsonPusher): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..9569574fce4578cfd94a0ecd4a9c139cabf23c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.internal.session.notification.DefaultProcessEventForPushTask +import org.matrix.android.sdk.internal.session.notification.DefaultPushRuleService +import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask +import org.matrix.android.sdk.internal.session.room.notification.DefaultSetRoomNotificationStateTask +import org.matrix.android.sdk.internal.session.room.notification.SetRoomNotificationStateTask +import retrofit2.Retrofit + +@Module +internal abstract class PushersModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesPushersAPI(retrofit: Retrofit): PushersAPI { + return retrofit.create(PushersAPI::class.java) + } + + @JvmStatic + @Provides + fun providesPushRulesApi(retrofit: Retrofit): PushRulesApi { + return retrofit.create(PushRulesApi::class.java) + } + } + + @Binds + abstract fun bindPusherService(service: DefaultPushersService): PushersService + + @Binds + abstract fun bindConditionResolver(resolver: DefaultConditionResolver): ConditionResolver + + @Binds + abstract fun bindGetPushersTask(task: DefaultGetPushersTask): GetPushersTask + + @Binds + abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask + + @Binds + abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask + + @Binds + abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask + + @Binds + abstract fun bindUpdatePushRuleEnableStatusTask(task: DefaultUpdatePushRuleEnableStatusTask): UpdatePushRuleEnableStatusTask + + @Binds + abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask + + @Binds + abstract fun bindUpdatePushRuleActionTask(task: DefaultUpdatePushRuleActionsTask): UpdatePushRuleActionsTask + + @Binds + abstract fun bindRemovePushRuleTask(task: DefaultRemovePushRuleTask): RemovePushRuleTask + + @Binds + abstract fun bindSetRoomNotificationStateTask(task: DefaultSetRoomNotificationStateTask): SetRoomNotificationStateTask + + @Binds + abstract fun bindPushRuleService(service: DefaultPushRuleService): PushRuleService + + @Binds + abstract fun bindProcessEventForPushTask(task: DefaultProcessEventForPushTask): ProcessEventForPushTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb46c1342d2a115f641b9c9aaa5aa1e339fd3360 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface RemovePushRuleTask : Task<RemovePushRuleTask.Params, Unit> { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultRemovePushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : RemovePushRuleTask { + + override suspend fun execute(params: RemovePushRuleTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf8cd1e10bd4f8f4d4c8ed61cb4c19554ccfd4db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface RemovePusherTask : Task<RemovePusherTask.Params, Unit> { + data class Params(val pushKey: String, + val pushAppId: String) +} + +internal class DefaultRemovePusherTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : RemovePusherTask { + + override suspend fun execute(params: RemovePusherTask.Params) { + monarchy.awaitTransaction { realm -> + val existingEntity = PusherEntity.where(realm, params.pushKey).findFirst() + existingEntity?.state = PusherState.UNREGISTERING + } + + val existing = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + PusherEntity.where(realm, params.pushKey).findFirst()?.asDomain() + } ?: throw Exception("No existing pusher") + + val deleteBody = JsonPusher( + pushKey = params.pushKey, + appId = params.pushAppId, + // kind null deletes the pusher + kind = null, + appDisplayName = existing.appDisplayName ?: "", + deviceDisplayName = existing.deviceDisplayName ?: "", + profileTag = existing.profileTag ?: "", + lang = existing.lang, + data = JsonPusherData(existing.data.url, existing.data.format), + append = false + ) + executeRequest<Unit>(eventBus) { + apiCall = pushersAPI.setPusher(deleteBody) + } + monarchy.awaitTransaction { + PusherEntity.where(it, params.pushKey).findFirst()?.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..7761544d482d31288fc6b38b98ff2e3b4e96395a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * Save the push rules in DB + */ +internal interface SavePushRulesTask : Task<SavePushRulesTask.Params, Unit> { + data class Params(val pushRules: GetPushRulesResponse) +} + +internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : SavePushRulesTask { + + override suspend fun execute(params: SavePushRulesTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current push rules + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = params.pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d68888a3f5ac5ae2a4b043aa1a72b7977305e0e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdatePushRuleActionsTask : Task<UpdatePushRuleActionsTask.Params, Unit> { + data class Params( + val kind: RuleKind, + val oldPushRule: PushRule, + val newPushRule: PushRule + ) +} + +internal class DefaultUpdatePushRuleActionsTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : UpdatePushRuleActionsTask { + + override suspend fun execute(params: UpdatePushRuleActionsTask.Params) { + if (params.oldPushRule.enabled != params.newPushRule.enabled) { + // First change enabled state + executeRequest<Unit>(eventBus) { + apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled) + } + } + + if (params.newPushRule.enabled) { + // Also ensure the actions are up to date + val body = mapOf("actions" to params.newPushRule.actions) + + executeRequest<Unit>(eventBus) { + apiCall = pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f9ac3edc0d5bd2dbdc2bbd2351c587c2db92a19 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdatePushRuleEnableStatusTask : Task<UpdatePushRuleEnableStatusTask.Params, Unit> { + data class Params(val kind: RuleKind, + val pushRule: PushRule, + val enabled: Boolean) +} + +internal class DefaultUpdatePushRuleEnableStatusTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : UpdatePushRuleEnableStatusTask { + + override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt new file mode 100644 index 0000000000000000000000000000000000000000..27a51594c3accc7d280bcaaaa71169f2ac555d37 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import java.security.InvalidParameterException +import javax.inject.Inject + +internal class DefaultRoom @Inject constructor(override val roomId: String, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val timelineService: TimelineService, + private val sendService: SendService, + private val draftService: DraftService, + private val stateService: StateService, + private val uploadsService: UploadsService, + private val reportingService: ReportingService, + private val roomCallService: RoomCallService, + private val readService: ReadService, + private val typingService: TypingService, + private val tagsService: TagsService, + private val cryptoService: CryptoService, + private val relationService: RelationService, + private val roomMembersService: MembershipService, + private val roomPushRuleService: RoomPushRuleService, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask) : + Room, + TimelineService by timelineService, + SendService by sendService, + DraftService by draftService, + StateService by stateService, + UploadsService by uploadsService, + ReportingService by reportingService, + RoomCallService by roomCallService, + ReadService by readService, + TypingService by typingService, + TagsService by tagsService, + RelationService by relationService, + MembershipService by roomMembersService, + RoomPushRuleService by roomPushRuleService { + + override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> { + return roomSummaryDataSource.getRoomSummaryLive(roomId) + } + + override fun roomSummary(): RoomSummary? { + return roomSummaryDataSource.getRoomSummary(roomId) + } + + override fun isEncrypted(): Boolean { + return cryptoService.isRoomEncrypted(roomId) + } + + override fun encryptionAlgorithm(): String? { + return cryptoService.getEncryptionAlgorithm(roomId) + } + + override fun shouldEncryptForInvitedMembers(): Boolean { + return cryptoService.shouldEncryptForInvitedMembers(roomId) + } + + override fun enableEncryption(algorithm: String, callback: MatrixCallback<Unit>) { + when { + isEncrypted() -> { + callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) + } + algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { + callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) + } + else -> { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = null, + eventType = EventType.STATE_ROOM_ENCRYPTION, + body = mapOf( + "algorithm" to algorithm + )) + + sendStateTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt new file mode 100644 index 0000000000000000000000000000000000000000..12d9d5bcdc0aa7d091552717ec8086e2fb1aa589 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, + private val taskExecutor: TaskExecutor) : RoomDirectoryService { + + override fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback<PublicRoomsResponse>): Cancelable { + return getPublicRoomTask + .configureWith(GetPublicRoomTask.Params(server, publicRoomsParams)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable { + return getThirdPartyProtocolsTask + .configureWith { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt new file mode 100644 index 0000000000000000000000000000000000000000..17c724368d30c03a1f7b56eb6526fadac745ba66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultRoomService @Inject constructor( + private val createRoomTask: CreateRoomTask, + private val joinRoomTask: JoinRoomTask, + private val markAllRoomsReadTask: MarkAllRoomsReadTask, + private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, + private val roomIdByAliasTask: GetRoomIdByAliasTask, + private val roomGetter: RoomGetter, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val taskExecutor: TaskExecutor +) : RoomService { + + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable { + return createRoomTask + .configureWith(createRoomParams) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getRoom(roomId: String): Room? { + return roomGetter.getRoom(roomId) + } + + override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + return roomGetter.getDirectRoomWith(otherUserId) + } + + override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return roomSummaryDataSource.getRoomSummary(roomIdOrAlias) + } + + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary> { + return roomSummaryDataSource.getRoomSummaries(queryParams) + } + + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> { + return roomSummaryDataSource.getRoomSummariesLive(queryParams) + } + + override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> { + return roomSummaryDataSource.getBreadcrumbs(queryParams) + } + + override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> { + return roomSummaryDataSource.getBreadcrumbsLive(queryParams) + } + + override fun onRoomDisplayed(roomId: String): Cancelable { + return updateBreadcrumbsTask + .configureWith(UpdateBreadcrumbsTask.Params(roomId)) + .executeBy(taskExecutor) + } + + override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { + return joinRoomTask + .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable { + return markAllRoomsReadTask + .configureWith(MarkAllRoomsReadTask.Params(roomIds)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable { + return roomIdByAliasTask + .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> { + return roomChangeMembershipStateDataSource.getLiveStates() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..4893947fc334cfc70a2219c1859ee701ecf85881 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -0,0 +1,573 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PollSummaryContent +import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent +import org.matrix.android.sdk.api.session.room.model.VoteInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.create +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +enum class VerificationState { + REQUEST, + WAITING, + CANCELED_BY_ME, + CANCELED_BY_OTHER, + DONE +} + +fun VerificationState.isCanceled(): Boolean { + return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER +} + +// State transition with control +private fun VerificationState?.toState(newState: VerificationState): VerificationState { + // Cancel is always prioritary ? + // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to + // consider as canceled + if (newState.isCanceled()) { + return newState + } + // never move out of cancel + if (this?.isCanceled() == true) { + return this + } + return newState +} + +internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, + private val cryptoService: CryptoService +) : EventInsertLiveProcessor { + + private val allowedTypes = listOf( + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + // TODO Add ? + // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return allowedTypes.contains(eventType) + } + + override suspend fun process(realm: Realm, event: Event) { + try { // Temporary catch, should be removed + val roomId = event.roomId + if (roomId == null) { + Timber.w("Event has no room id ${event.eventId}") + return + } + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + when (event.type) { + EventType.REACTION -> { + // we got a reaction!! + Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") + handleReaction(event, roomId, realm, userId, isLocalEcho) + } + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + + EventAnnotationsSummaryEntity.where(realm, event.eventId + ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId + ?: "").findFirst()?.let { tet -> + tet.annotations = it + } + } + } + + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } else if (content?.relatesTo?.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, content, roomId, isLocalEcho) + } + } + + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + event.content.toModel<MessageRelationContent>()?.relatesTo?.let { + if (it.type == RelationType.REFERENCE && it.eventId != null) { + handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + } + } + } + + EventType.ENCRYPTED -> { + // Relation type is in clear + val encryptedEventContent = event.content.toModel<EncryptedEventContent>() + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE + || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE + ) { + event.getClearContent().toModel<MessageContent>()?.let { + if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } + } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + encryptedEventContent.relatesTo.eventId?.let { + handleVerification(realm, event, roomId, isLocalEcho, it, userId) + } + } + } + } + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") +// val unsignedData = EventMapper.map(eventToPrune).unsignedData +// ?: UnsignedData(null, null) + + // was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + } + } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } + } + } + else -> Timber.v("UnHandled event ${event.eventId}") + } + } catch (t: Throwable) { + Timber.e(t, "## Should not happen ") + } + } + + // OPT OUT serer aggregation until API mature enough + private val SHOULD_HANDLE_SERVER_AGREGGATION = false + + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { + val eventId = event.eventId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return + val newContent = content.newContent ?: return + // ok, this is a replace + val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId) + + // we have it + val existingSummary = existing.editSummary + if (existingSummary == null) { + Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)") + // create the edit summary + val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) + editSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + editSummary.lastEditTs = 0 + editSummary.sourceLocalEchoEvents.add(eventId) + } else { + editSummary.lastEditTs = event.originServerTs ?: 0 + editSummary.sourceEvents.add(eventId) + } + + existing.editSummary = editSummary + } else { + if (existingSummary.sourceEvents.contains(eventId)) { + // ignore this event, we already know it (??) + Timber.v("###REPLACE ignoring event for summary, it's known $eventId") + return + } + val txId = event.unsignedData?.transactionId + // is it a remote echo? + if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) { + // ok it has already been managed + Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") + existingSummary.sourceLocalEchoEvents.remove(txId) + existingSummary.sourceEvents.add(event.eventId) + } else if ( + isLocalEcho // do not rely on ts for local echo, take it + || event.originServerTs ?: 0 >= existingSummary.lastEditTs + ) { + Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") + if (!isLocalEcho) { + // Do not take local echo originServerTs here, could mess up ordering (keep old ts) + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + } + existingSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + existingSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingSummary.sourceEvents.add(eventId) + } + } else { + // ignore this event for the summary (back paginate) + if (!isLocalEcho) { + existingSummary.sourceEvents.add(eventId) + } + Timber.v("###REPLACE ignoring event for summary, it's to old $eventId") + } + } + } + + private fun handleResponse(realm: Realm, + userId: String, + event: Event, + content: MessageContent, + roomId: String, + isLocalEcho: Boolean, + relatedEventId: String? = null) { + val eventId = event.eventId ?: return + val senderId = event.senderId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return + val eventTimestamp = event.originServerTs ?: return + + // ok, this is a poll response + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("## POLL creating new relation summary for $targetEventId") + existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) + } + + // we have it + val existingPollSummary = existing.pollResponseSummary + ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { + existing.pollResponseSummary = it + } + + val closedTime = existingPollSummary?.closedTime + if (closedTime != null && eventTimestamp > closedTime) { + Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") + return + } + + val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent() + + if (existingPollSummary!!.sourceEvents.contains(eventId)) { + // ignore this event, we already know it (??) + Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") + return + } + val txId = event.unsignedData?.transactionId + // is it a remote echo? + if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { + // ok it has already been managed + Timber.v("## POLL Receiving remote echo of response eventId:$eventId") + existingPollSummary.sourceLocalEchoEvents.remove(txId) + existingPollSummary.sourceEvents.add(event.eventId) + return + } + + val responseContent = event.content.toModel<MessagePollResponseContent>() ?: return Unit.also { + Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}") + } + + val optionIndex = responseContent.relatesTo?.option ?: return Unit.also { + Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") + } + + val votes = sumModel.votes?.toMutableList() ?: ArrayList() + val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } + if (existingVoteIndex != -1) { + // Is the vote newer? + val existingVote = votes[existingVoteIndex] + if (existingVote.voteTimestamp < eventTimestamp) { + // Take the new one + votes[existingVoteIndex] = VoteInfo(senderId, optionIndex, eventTimestamp) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } else { + Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") + } + } else { + votes.add(VoteInfo(senderId, optionIndex, eventTimestamp)) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } + sumModel.votes = votes + if (isLocalEcho) { + existingPollSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingPollSummary.sourceEvents.add(eventId) + } + + existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent()) + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + if (SHOULD_HANDLE_SERVER_AGREGGATION) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId) + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs + ?: 0 // TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + // TODO how to handle that + } + } + } + } + } + + private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + val content = event.content.toModel<ReactionContent>() + if (content == null) { + Timber.e("Malformed reaction content ${event.content}") + return + } + // rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val relatedEventID = content.relatesTo.eventId + val reactionEventId = event.eventId + Timber.v("Reaction $reactionEventId relates to $relatedEventID") + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID) + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 + } else { + Timber.v("Adding synced reaction $reaction") + sum.count = 1 + sum.sourceEvents.add(reactionEventId) + } + sum.addedByMe = sum.addedByMe || (userId == event.senderId) + eventSummary.reactionsSummary.add(sum) + } else { + // is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(reactionEventId)) { + // check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + // ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(reactionEventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + } else { + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(reactionEventId) + } + + sum.addedByMe = sum.addedByMe || (userId == event.senderId) + } + } + } + } else { + Timber.e("Unknown relation type ${content.relatesTo?.type} for event ${event.eventId}") + } + } + + /** + * Called when an event is deleted + */ + private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + Timber.d("Handle redaction of m.replace") + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + if (eventSummary == null) { + Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") + return + } + val sourceEvents = eventSummary.editSummary?.sourceEvents + val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) + if (sourceToDiscard == null) { + Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + return + } + // Need to remove this event from the redaction list and compute new aggregation state + sourceEvents.removeAt(sourceToDiscard) + val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() + if (previousEdit == null) { + // revert to original + eventSummary.editSummary?.deleteFromRealm() + } else { + // I have the last event + ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent -> + eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs + ?: System.currentTimeMillis() + eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) + } ?: run { + Timber.e("Failed to udate edited summary") + // TODO how to reccover that + } + } + } + + fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + Timber.v("REDACTION of reaction ${eventToPrune.eventId}") + // delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionKey = reactionContent.relatesTo.key + Timber.v("REMOVE reaction for key $reactionKey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey) + .findFirst()?.let { aggregation -> + Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})") + Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}") + if (aggregation.sourceEvents.contains(eventToPrune.eventId)) { + Timber.v("REMOVE reaction for key $reactionKey") + aggregation.sourceEvents.remove(eventToPrune.eventId) + Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}") + aggregation.count = aggregation.count - 1 + if (eventToPrune.sender == userId) { + // Was it a redact on my reaction? + aggregation.addedByMe = false + } + if (aggregation.count == 0) { + // delete! + aggregation.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionKey") + } + } + + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) + + val verifSummary = eventSummary.referencesSummaryEntity + ?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also { + eventSummary.referencesSummaryEntity = it + } + + val txId = event.unsignedData?.transactionId + + if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) { + // ok it has already been handled + } else { + ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>() + var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>() + ?: ReferencesAggregatedContent(VerificationState.REQUEST) + // TODO ignore invalid messages? e.g a START after a CANCEL? + // i.e. never change state if already canceled/done + val currentState = data.verificationState + val newState = when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING) + EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else { + VerificationState.CANCELED_BY_OTHER + }) + EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE) + else -> VerificationState.REQUEST + } + + data = data.copy(verificationState = newState) + verifSummary.content = ContentMapper.map(data.toContent()) + } + + if (isLocalEcho) { + verifSummary.sourceLocalEcho.add(event.eventId) + } else { + verifSummary.sourceLocalEcho.remove(txId) + verifSummary.sourceEvents.add(event.eventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..25dcc69fa8d1873ad61782e07e45edb0fc71190c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -0,0 +1,383 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.session.room.create.CreateRoomBody +import org.matrix.android.sdk.internal.session.room.create.CreateRoomResponse +import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse +import org.matrix.android.sdk.internal.session.room.membership.RoomMembersResponse +import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.session.room.tags.TagBody +import org.matrix.android.sdk.internal.session.room.timeline.EventContextResponse +import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse +import org.matrix.android.sdk.internal.session.room.typing.TypingBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface RoomAPI { + + /** + * Get the third party server protocols. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") + fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>> + + /** + * Lists the public rooms on the server, with optional filter. + * This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-publicrooms + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms") + fun publicRooms(@Query("server") server: String?, + @Body publicRoomsParams: PublicRoomsParams + ): Call<PublicRoomsResponse> + + /** + * Create a room. + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom + * Set all the timeouts to 1 minute, because if the server takes time to answer, we will not execute the + * create direct chat request if any + * + * @param param the creation room parameter + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom") + fun createRoom(@Body param: CreateRoomBody): Call<CreateRoomResponse> + + /** + * Get a list of messages starting from a reference. + * + * @param roomId the room id + * @param from the token identifying where to start. Required. + * @param dir The direction to return messages from. Required. + * @param limit the maximum number of messages to retrieve. Optional. + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages") + fun getRoomMessagesFrom(@Path("roomId") roomId: String, + @Query("from") from: String, + @Query("dir") dir: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? + ): Call<PaginationResponse> + + /** + * Get all members of a room + * + * @param roomId the room id where to get the members + * @param syncToken the sync token (optional) + * @param membership to include only one type of membership (optional) + * @param notMembership to exclude one type of membership (optional) + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members") + fun getMembers(@Path("roomId") roomId: String, + @Query("at") syncToken: String?, + @Query("membership") membership: String?, + @Query("not_membership") notMembership: String? + ): Call<RoomMembersResponse> + + /** + * Send an event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") + fun send(@Path("txId") txId: String, + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call<SendResponse> + + /** + * Send an event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content as string + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") + fun send(@Path("txId") txId: String, + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Body content: String? + ): Call<SendResponse> + + /** + * Get the context surrounding an event. + * + * @param roomId the room id + * @param eventId the event Id + * @param limit the maximum number of messages to retrieve + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}") + fun getContextOfEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? = null): Call<EventContextResponse> + + /** + * Retrieve an event from its room id / events id + * + * @param roomId the room id + * @param eventId the event Id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") + fun getEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String): Call<Event> + + /** + * Send read markers. + * + * @param roomId the room id + * @param markers the read markers + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") + fun sendReadMarker(@Path("roomId") roomId: String, + @Body markers: Map<String, String>): Call<Unit> + + /** + * Invite a user to the given room. + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-rooms-roomid-invite + * + * @param roomId the room id + * @param body a object that just contains a user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") + fun invite(@Path("roomId") roomId: String, + @Body body: InviteBody): Call<Unit> + + /** + * Invite a user to a room, using a ThreePid + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101 + * @param roomId Required. The room identifier (not alias) to which to invite the user. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") + fun invite3pid(@Path("roomId") roomId: String, + @Body body: ThreePidInviteBody): Call<Unit> + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param params the request parameters + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}") + fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Body params: JsonDict): Call<Unit> + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param stateKey the state keys + * @param params the request parameters + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}") + fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Path("state_key") stateKey: String, + @Body params: JsonDict): Call<Unit> + + /** + * Send a relation event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") + fun sendRelation(@Path("roomId") roomId: String, + @Path("parentId") parent_id: String, + @Path("relation_type") relationType: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call<SendResponse> + + /** + * Paginate relations for event based in normal topological order + * + * @param relationType filter for this relation type + * @param eventType filter for this event type + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") + fun getRelations(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Path("relationType") relationType: String, + @Path("eventType") eventType: String + ): Call<RelationsResponse> + + /** + * Join the given room. + * + * @param roomIdOrAlias the room id or alias + * @param viaServers the servers to attempt to join the room through + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") + fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, + @Query("server_name") viaServers: List<String>, + @Body params: Map<String, String?>): Call<JoinRoomResponse> + + /** + * Leave the given room. + * + * @param roomId the room id + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") + fun leave(@Path("roomId") roomId: String, + @Body params: Map<String, String?>): Call<Unit> + + /** + * Ban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the banned user object (userId and reason for ban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban") + fun ban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call<Unit> + + /** + * unban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the unbanned user object (userId and reason for unban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban") + fun unban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call<Unit> + + /** + * Kick a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the kicked user object (userId and reason for kicking) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick") + fun kick(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call<Unit> + + /** + * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. + * This cannot be undone. + * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete + * @param reason json containing reason key {"reason": "Indecent material"} + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") + fun redactEvent( + @Path("txnId") txId: String, + @Path("roomId") roomId: String, + @Path("eventId") parent_id: String, + @Body reason: Map<String, String> + ): Call<SendResponse> + + /** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * + * @param roomId the room id + * @param eventId the event to report content + * @param body body containing score and reason + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") + fun reportContent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Body body: ReportContentBody): Call<Unit> + + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription> + + /** + * Add alias to the room. + * @param roomAlias the room alias. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun addRoomAlias(@Path("roomAlias") roomAlias: String, + @Body body: AddRoomAliasBody): Call<Unit> + + /** + * Inform that the user is starting to type or has stopped typing + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}") + fun sendTypingState(@Path("roomId") roomId: String, + @Path("userId") userId: String, + @Body body: TypingBody): Call<Unit> + + /** + * Room tagging + */ + + /** + * Add a tag to a room. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun putTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String, + @Body body: TagBody): Call<Unit> + + /** + * Delete a tag from a room. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun deleteTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..6851780a624792a8549b206dd9214c370a32e6c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +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.RoomAvatarContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm +import javax.inject.Inject + +internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { + + /** + * Compute the room avatar url + * @param realm: the current instance of realm + * @param roomId the roomId of the room to resolve avatar + * @return the room avatar url, can be a fallback to a room member avatar or null + */ + fun resolve(realm: Realm, roomId: String): String? { + var res: String? + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root + res = ContentMapper.map(roomName?.content).toModel<RoomAvatarContent>()?.avatarUrl + if (!res.isNullOrEmpty()) { + return res + } + val roomMembers = RoomMemberHelper(realm, roomId) + val members = roomMembers.queryActiveRoomMembersEvent().findAll() + // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) + if (members.size == 1) { + res = members.firstOrNull()?.avatarUrl + } else if (members.size == 2) { + val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl + } + return res + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac8bdb39922e094885e2754d15c84f8ce5d92f23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService +import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService +import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService +import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService +import org.matrix.android.sdk.internal.session.room.read.DefaultReadService +import org.matrix.android.sdk.internal.session.room.relation.DefaultRelationService +import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportingService +import org.matrix.android.sdk.internal.session.room.send.DefaultSendService +import org.matrix.android.sdk.internal.session.room.state.DefaultStateService +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService +import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService +import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal interface RoomFactory { + fun create(roomId: String): Room +} + +@SessionScope +internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val timelineServiceFactory: DefaultTimelineService.Factory, + private val sendServiceFactory: DefaultSendService.Factory, + private val draftServiceFactory: DefaultDraftService.Factory, + private val stateServiceFactory: DefaultStateService.Factory, + private val uploadsServiceFactory: DefaultUploadsService.Factory, + private val reportingServiceFactory: DefaultReportingService.Factory, + private val roomCallServiceFactory: DefaultRoomCallService.Factory, + private val readServiceFactory: DefaultReadService.Factory, + private val typingServiceFactory: DefaultTypingService.Factory, + private val tagsServiceFactory: DefaultTagsService.Factory, + private val relationServiceFactory: DefaultRelationService.Factory, + private val membershipServiceFactory: DefaultMembershipService.Factory, + private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask) : + RoomFactory { + + override fun create(roomId: String): Room { + return DefaultRoom( + roomId = roomId, + roomSummaryDataSource = roomSummaryDataSource, + timelineService = timelineServiceFactory.create(roomId), + sendService = sendServiceFactory.create(roomId), + draftService = draftServiceFactory.create(roomId), + stateService = stateServiceFactory.create(roomId), + uploadsService = uploadsServiceFactory.create(roomId), + reportingService = reportingServiceFactory.create(roomId), + roomCallService = roomCallServiceFactory.create(roomId), + readService = readServiceFactory.create(roomId), + typingService = typingServiceFactory.create(roomId), + tagsService = tagsServiceFactory.create(roomId), + cryptoService = cryptoService, + relationService = relationServiceFactory.create(roomId), + roomMembersService = membershipServiceFactory.create(roomId), + roomPushRuleService = roomPushRuleServiceFactory.create(roomId), + taskExecutor = taskExecutor, + sendStateTask = sendStateTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt new file mode 100644 index 0000000000000000000000000000000000000000..38dcad231167b82ab7c92317095059432461a3cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm +import javax.inject.Inject + +internal interface RoomGetter { + fun getRoom(roomId: String): Room? + + fun getDirectRoomWith(otherUserId: String): Room? +} + +@SessionScope +internal class DefaultRoomGetter @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val roomFactory: RoomFactory +) : RoomGetter { + + override fun getRoom(roomId: String): Room? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + createRoom(realm, roomId) + } + } + + override fun getDirectRoomWith(otherUserId: String): Room? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .findAll() + .filter { dm -> dm.otherMemberIds.contains(otherUserId) } + .map { it.roomId } + .firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() } + ?.let { roomId -> createRoom(realm, roomId) } + } + } + + private fun createRoom(realm: Realm, roomId: String): Room? { + return RoomEntity.where(realm, roomId).findFirst() + ?.let { roomFactory.create(roomId) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f21ee84f67bc534a34f67eeea99a2327a20d56f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultAddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.joining.DefaultInviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.DefaultJoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask +import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultFetchEditHistoryTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask +import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask +import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask +import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask +import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.tags.AddTagToRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DefaultAddTagToRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DefaultDeleteTagFromRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DeleteTagFromRoomTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultPaginationTask +import org.matrix.android.sdk.internal.session.room.timeline.FetchTokenAndPaginateTask +import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask +import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask +import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask +import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask +import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import retrofit2.Retrofit + +@Module +internal abstract class RoomModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesRoomAPI(retrofit: Retrofit): RoomAPI { + return retrofit.create(RoomAPI::class.java) + } + + @Provides + @JvmStatic + fun providesParser(): Parser { + return Parser.builder().build() + } + + @Provides + @JvmStatic + fun providesHtmlRenderer(): HtmlRenderer { + return HtmlRenderer + .builder() + .build() + } + + @Provides + @JvmStatic + fun providesTextContentRenderer(): TextContentRenderer { + return TextContentRenderer + .builder() + .build() + } + } + + @Binds + abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory + + @Binds + abstract fun bindRoomGetter(getter: DefaultRoomGetter): RoomGetter + + @Binds + abstract fun bindRoomService(service: DefaultRoomService): RoomService + + @Binds + abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService + + @Binds + abstract fun bindFileService(service: DefaultFileService): FileService + + @Binds + abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask + + @Binds + abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask + + @Binds + abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask + + @Binds + abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask + + @Binds + abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask + + @Binds + abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask + + @Binds + abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask + + @Binds + abstract fun bindMembershipAdminTask(task: DefaultMembershipAdminTask): MembershipAdminTask + + @Binds + abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask + + @Binds + abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask + + @Binds + abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask + + @Binds + abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask + + @Binds + abstract fun bindUpdateQuickReactionTask(task: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask + + @Binds + abstract fun bindSendStateTask(task: DefaultSendStateTask): SendStateTask + + @Binds + abstract fun bindReportContentTask(task: DefaultReportContentTask): ReportContentTask + + @Binds + abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask + + @Binds + abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask + + @Binds + abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchTokenAndPaginateTask): FetchTokenAndPaginateTask + + @Binds + abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask + + @Binds + abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask + + @Binds + abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask + + @Binds + abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask + + @Binds + abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask + + @Binds + abstract fun bindAddTagToRoomTask(task: DefaultAddTagToRoomTask): AddTagToRoomTask + + @Binds + abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5938d592a8de6871b6a87856b49c05024916f30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddRoomAliasBody( + /** + * Required. The room id which the alias will be added to. + */ + @Json(name = "room_id") val roomId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..510bd25d9c7a22d4051ed6b97bab5f504550f1cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddRoomAliasTask : Task<AddRoomAliasTask.Params, Unit> { + data class Params( + val roomId: String, + val roomAlias: String + ) +} + +internal class DefaultAddRoomAliasTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : AddRoomAliasTask { + + override suspend fun execute(params: AddRoomAliasTask.Params) { + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.addRoomAlias( + roomAlias = params.roomAlias, + body = AddRoomAliasBody( + roomId = params.roomId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e1d119c432f85111f3e07533b060a31d9d9cccac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.findByAlias +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> { + data class Params( + val roomAlias: String, + val searchOnServer: Boolean + ) +} + +internal class DefaultGetRoomIdByAliasTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetRoomIdByAliasTask { + + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> { + var roomId = Realm.getInstance(monarchy.realmConfiguration).use { + RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId + } + return if (roomId != null) { + Optional.from(roomId) + } else if (!params.searchOnServer) { + Optional.from<String>(null) + } else { + roomId = executeRequest<RoomAliasDescription>(eventBus) { + apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) + }.roomId + Optional.from(roomId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac0b59c9167f0f8140606715e4ffea8b2fdc5d29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomAliasDescription( + /** + * The room ID for this alias. + */ + @Json(name = "room_id") val roomId: String, + + /** + * A list of servers that are aware of this room ID. + */ + @Json(name = "servers") val servers: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt new file mode 100644 index 0000000000000000000000000000000000000000..3d764e001cc56507f6b8d333225060eef28213f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.call + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.internal.session.room.RoomGetter + +internal class DefaultRoomCallService @AssistedInject constructor( + @Assisted private val roomId: String, + private val roomGetter: RoomGetter +) : RoomCallService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RoomCallService + } + + override fun canStartCall(): Boolean { + return roomGetter.getRoom(roomId)?.roomSummary()?.canStartCall.orFalse() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9ae5e7a6b2f0bfe77ba1e6baa946a3562c3d401 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody + +/** + * Parameter to create a room + */ +@JsonClass(generateAdapter = true) +internal data class CreateRoomBody( + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + @Json(name = "visibility") + val visibility: RoomDirectoryVisibility?, + + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + @Json(name = "room_alias_name") + val roomAliasName: String?, + + /** + * If this is included, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + @Json(name = "name") + val name: String?, + + /** + * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + @Json(name = "topic") + val topic: String?, + + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + @Json(name = "invite") + val invitedUserIds: List<String>?, + + /** + * A list of objects representing third party IDs to invite into the room. + */ + @Json(name = "invite_3pid") + val invite3pids: List<ThreePidInviteBody>?, + + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + @Json(name = "creation_content") + val creationContent: Any?, + + /** + * A list of state events to set in the new room. + * This allows the user to override the default state events set in the new room. + * The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by presets, but gets overridden by name and topic keys. + */ + @Json(name = "initial_state") + val initialStates: List<Event>?, + + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. + */ + @Json(name = "preset") + val preset: CreateRoomPreset?, + + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + @Json(name = "is_direct") + val isDirect: Boolean?, + + /** + * The power level content to override in the default power level event + */ + @Json(name = "power_level_content_override") + val powerLevelContentOverride: PowerLevelsContent? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..6e450e5428d8452afba899729a3d9a8773ed027c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import java.security.InvalidParameterException +import javax.inject.Inject + +internal class CreateRoomBodyBuilder @Inject constructor( + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + private val crossSigningService: CrossSigningService, + private val deviceListManager: DeviceListManager, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) { + + suspend fun build(params: CreateRoomParams): CreateRoomBody { + val invite3pids = params.invite3pids + .takeIf { it.isNotEmpty() } + ?.let { invites -> + // This can throw Exception if Identity server is not configured + ensureIdentityTokenTask.execute(Unit) + + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + + invites.map { + ThreePidInviteBody( + id_server = identityServerUrlWithoutProtocol, + id_access_token = identityServerAccessToken, + medium = it.toMedium(), + address = it.value + ) + } + } + + val initialStates = listOfNotNull( + buildEncryptionWithAlgorithmEvent(params), + buildHistoryVisibilityEvent(params) + ) + .takeIf { it.isNotEmpty() } + + return CreateRoomBody( + visibility = params.visibility, + roomAliasName = params.roomAliasName, + name = params.name, + topic = params.topic, + invitedUserIds = params.invitedUserIds, + invite3pids = invite3pids, + creationContent = params.creationContent, + initialStates = initialStates, + preset = params.preset, + isDirect = params.isDirect, + powerLevelContentOverride = params.powerLevelContentOverride + ) + } + + private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? { + return params.historyVisibility + ?.let { + val contentMap = mapOf("history_visibility" to it) + + Event( + type = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + content = contentMap.toContent()) + } + } + + /** + * Add the crypto algorithm to the room creation parameters. + */ + private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? { + if (params.algorithm == null + && canEnableEncryption(params)) { + // Enable the encryption + params.enableEncryption() + } + return params.algorithm + ?.let { + if (it != MXCRYPTO_ALGORITHM_MEGOLM) { + throw InvalidParameterException("Unsupported algorithm: $it") + } + val contentMap = mapOf("algorithm" to it) + + Event( + type = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + content = contentMap.toContent() + ) + } + } + + private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { + return (params.enableEncryptionIfInvitedUsersSupportIt + && crossSigningService.isCrossSigningVerified() + && params.invite3pids.isEmpty()) + && params.invitedUserIds.isNotEmpty() + && params.invitedUserIds.let { userIds -> + val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) + + userIds.all { userId -> + keys.map[userId].let { deviceMap -> + if (deviceMap.isNullOrEmpty()) { + // A user has no device, so do not enable encryption + false + } else { + // Check that every user's device have at least one key + deviceMap.values.all { !it.keys.isNullOrEmpty() } + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..cfc63bcb7e8c817d0daca3efcfdfed5d67b67fa1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class CreateRoomResponse( + /** + * Required. The created room's ID. + */ + @Json(name = "room_id") val roomId: String +) + +internal typealias JoinRoomResponse = CreateRoomResponse diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e13d5305b539e2bea83d7d7793df61aa5780dfd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface CreateRoomTask : Task<CreateRoomParams, String> + +internal class DefaultCreateRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val createRoomBodyBuilder: CreateRoomBodyBuilder, + private val eventBus: EventBus +) : CreateRoomTask { + + override suspend fun execute(params: CreateRoomParams): String { + val createRoomBody = createRoomBodyBuilder.build(params) + + val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) { + apiCall = roomAPI.createRoom(createRoomBody) + } + val roomId = createRoomResponse.roomId + // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { + throw CreateRoomFailure.CreatedWithTimeout + } + if (params.isDirect()) { + handleDirectChatCreation(params, roomId) + } + setReadMarkers(roomId) + return roomId + } + + private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) { + val otherUserId = params.getFirstInvitedUserId() + ?: throw IllegalStateException("You can't create a direct room without an invitedUser") + + monarchy.awaitTransaction { realm -> + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + this.directUserId = otherUserId + this.isDirect = true + } + } + val directChats = directChatsHelper.getLocalUserAccount() + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true) + return readMarkersTask.execute(setReadMarkerParams) + } + + /** + * Tells if the created room can be a direct chat one. + * + * @return true if it is a direct chat + */ + private fun CreateRoomParams.isDirect(): Boolean { + return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT + && isDirect == true + } + + /** + * @return the first invited user id + */ + private fun CreateRoomParams.getFirstInvitedUserId(): String? { + return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6de3fbd7171a51b2ec1664b578db3151fe826da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + val createRoomContent = event.getClearContent().toModel<RoomCreateContent>() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.STATE_ROOM_CREATE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..4d760d1b09c6c8ca282467dc304f7807b368831e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPublicRoomTask : Task<GetPublicRoomTask.Params, PublicRoomsResponse> { + data class Params( + val server: String?, + val publicRoomsParams: PublicRoomsParams + ) +} + +internal class DefaultGetPublicRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetPublicRoomTask { + + override suspend fun execute(params: GetPublicRoomTask.Params): PublicRoomsResponse { + return executeRequest(eventBus) { + apiCall = roomAPI.publicRooms(params.server, params.publicRoomsParams) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..39f1c60829469ed98fb92a80afd8386c97d48c10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetThirdPartyProtocolsTask : Task<Unit, Map<String, ThirdPartyProtocol>> + +internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetThirdPartyProtocolsTask { + + override suspend fun execute(params: Unit): Map<String, ThirdPartyProtocol> { + return executeRequest(eventBus) { + apiCall = roomAPI.thirdPartyProtocols() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt new file mode 100644 index 0000000000000000000000000000000000000000..dafa7df0ebd8e19e416285ad8808aa18a3a1e773 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.draft + +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, + private val draftRepository: DraftRepository, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) : DraftService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): DraftService + } + + /** + * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, + * or even move an existing draft to the top of the list + */ + override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.saveDraft(roomId, draft) + } + } + + override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.deleteDraft(roomId) + } + } + + override fun getDraftsLive(): LiveData<List<UserDraft>> { + return draftRepository.getDraftsLive(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc50b2d9906e50c17fe6e0bc98cf1f5611c57a62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.internal.database.mapper.DraftMapper +import org.matrix.android.sdk.internal.database.model.DraftEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.UserDraftsEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.createObject +import timber.log.Timber +import javax.inject.Inject + +class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + suspend fun saveDraft(roomId: String, userDraft: UserDraft) { + monarchy.awaitTransaction { + saveDraft(it, userDraft, roomId) + } + } + + suspend fun deleteDraft(roomId: String) { + monarchy.awaitTransaction { + deleteDraft(it, roomId) + } + } + + private fun deleteDraft(realm: Realm, roomId: String) { + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + + private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject<UserDraftsEntity>().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + + fun getDraftsLive(roomId: String): LiveData<List<UserDraft>> { + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + return "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt new file mode 100644 index 0000000000000000000000000000000000000000..91039f4c0f408ecdcc76300cd7f1bf9cbe46bfc1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.fetchCopied +import io.realm.Realm +import io.realm.RealmQuery + +internal class DefaultMembershipService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, + private val inviteThreePidTask: InviteThreePidTask, + private val joinTask: JoinRoomTask, + private val leaveRoomTask: LeaveRoomTask, + private val membershipAdminTask: MembershipAdminTask, + @UserId + private val userId: String +) : MembershipService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): MembershipService + } + + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable { + val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) + return loadRoomMembersTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun getRoomMember(userId: String): RoomMemberSummary? { + val roomMemberEntity = monarchy.fetchCopied { + RoomMemberHelper(it, roomId).getLastRoomMember(userId) + } + return roomMemberEntity?.asDomain() + } + + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMemberSummary> { + return monarchy.fetchAllMappedSync( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMemberSummary>> { + return monarchy.findAllMappedWithChanges( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery<RoomMemberSummaryEntity> { + return RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) + .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .apply { + if (queryParams.excludeSelf) { + notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + } + } + } + + override fun getNumberOfJoinedMembers(): Int { + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomMemberHelper(it, roomId).getNumberOfJoinedMembers() + } + } + + override fun ban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun kick(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { + val params = InviteTask.Params(roomId, userId, reason) + return inviteTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun invite3pid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + val params = InviteThreePidTask.Params(roomId, threePid) + return inviteThreePidTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { + val params = JoinRoomTask.Params(roomId, reason, viaServers) + return joinTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun leave(reason: String?, callback: MatrixCallback<Unit>): Cancelable { + val params = LeaveRoomTask.Params(roomId, reason) + return leaveRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e51a4605c8b0830a3536355a94e364e733df10fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit> { + + data class Params( + val roomId: String, + val excludeMembership: Membership? = null + ) +} + +internal class DefaultLoadRoomMembersTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val syncTokenStore: SyncTokenStore, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val eventBus: EventBus +) : LoadRoomMembersTask { + + override suspend fun execute(params: LoadRoomMembersTask.Params) { + if (areAllMembersAlreadyLoaded(params.roomId)) { + return + } + val lastToken = syncTokenStore.getLastToken() + val response = executeRequest<RoomMembersResponse>(eventBus) { + apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + } + insertInDb(response, params.roomId) + } + + private suspend fun insertInDb(response: RoomMembersResponse, roomId: String) { + monarchy.awaitTransaction { realm -> + // We ignore all the already known members + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + val now = System.currentTimeMillis() + for (roomMemberEvent in response.roomMemberEvents) { + if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) { + continue + } + val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it } + val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply { + eventId = roomMemberEvent.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) + } + roomEntity.areAllMembersLoaded = true + roomSummaryUpdater.update(realm, roomId, updateMembers = true) + } + } + + private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomEntity.where(it, roomId).findFirst()?.areAllMembersLoaded ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..00d54a62e7902e572d125a91dc0f6ec5e53718ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +/** + * This class holds information about rooms that current user is joining or leaving. + */ +@SessionScope +internal class RoomChangeMembershipStateDataSource @Inject constructor() { + + private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap()) + private val states = HashMap<String, ChangeMembershipState>() + + /** + * This will update local states to be synced with the server. + */ + fun setMembershipFromSync(roomId: String, membership: Membership) { + if (states.containsKey(roomId)) { + val newState = membership.toMembershipChangeState() + updateState(roomId, newState) + } + } + + fun updateState(roomId: String, state: ChangeMembershipState) { + states[roomId] = state + mutableLiveStates.postValue(states.toMap()) + } + + fun getLiveStates(): LiveData<Map<String, ChangeMembershipState>> { + return mutableLiveStates + } + + fun getState(roomId: String): ChangeMembershipState { + return states.getOrElse(roomId) { + ChangeMembershipState.Unknown + } + } + + private fun Membership.toMembershipChangeState(): ChangeMembershipState { + return when { + this == Membership.JOIN -> ChangeMembershipState.Joined + this.isLeft() -> ChangeMembershipState.Left + else -> ChangeMembershipState.Unknown + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt new file mode 100644 index 0000000000000000000000000000000000000000..d11226bdb16d249846f195e97fc68acfdd3b3610 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import android.content.Context +import org.matrix.android.sdk.R +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.RoomAliasesContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.UserId +import io.realm.Realm +import javax.inject.Inject + +/** + * This class computes room display name + */ +internal class RoomDisplayNameResolver @Inject constructor(private val context: Context, + @UserId private val userId: String +) { + + /** + * Compute the room display name + * + * @param realm: the current instance of realm + * @param roomId: the roomId to resolve the name of. + * @return the room display name + */ + fun resolve(realm: Realm, roomId: String): CharSequence { + // this algorithm is the one defined in + // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 + // calculateRoomName(room, userId) + + // For Lazy Loaded room, see algorithm here: + // https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn + var name: CharSequence? + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root + name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name + if (!name.isNullOrEmpty()) { + return name + } + val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root + name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias + if (!name.isNullOrEmpty()) { + return name + } + + val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + name = ContentMapper.map(aliases?.content).toModel<RoomAliasesContent>()?.aliases?.firstOrNull() + if (!name.isNullOrEmpty()) { + return name + } + + val roomMembers = RoomMemberHelper(realm, roomId) + val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() + + if (roomEntity?.membership == Membership.INVITE) { + val inviteMeEvent = roomMembers.getLastStateEvent(userId) + val inviterId = inviteMeEvent?.sender + name = if (inviterId != null) { + activeMembers.where() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, inviterId) + .findFirst() + ?.displayName + } else { + context.getString(R.string.room_displayname_room_invite) + } + } else if (roomEntity?.membership == Membership.JOIN) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { userId -> + roomMembers.getLastRoomMember(userId)?.takeIf { + it.membership == Membership.INVITE || it.membership == Membership.JOIN + } + } + } else { + activeMembers.where() + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .limit(3) + .findAll() + .createSnapshot() + } + val otherMembersCount = otherMembersSubset.count() + name = when (otherMembersCount) { + 0 -> context.getString(R.string.room_displayname_empty_room) + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + 2 -> context.getString(R.string.room_displayname_two_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) + ) + else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, + roomMembers.getNumberOfJoinedMembers() - 1, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) + } + } + return name ?: roomId + } + + /** See [org.matrix.android.sdk.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ + private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, + roomMemberHelper: RoomMemberHelper): String? { + if (roomMemberSummary == null) return null + val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) + return if (isUnique) { + roomMemberSummary.displayName + } else { + "${roomMemberSummary.displayName} (${roomMemberSummary.userId})" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..f526550918175be3b312fe9235500db6fccda867 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity + +internal object RoomMemberEntityFactory { + + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity { + val primaryKey = "${roomId}_$userId" + return RoomMemberSummaryEntity( + primaryKey = primaryKey, + userId = userId, + roomId = roomId, + displayName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl + ).apply { + membership = roomMember.membership + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..2821eb2fb986a47576c1fe1a4ee27aab5f143b0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.session.user.UserEntityFactory +import io.realm.Realm +import javax.inject.Inject + +internal class RoomMemberEventHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, event: Event): Boolean { + if (event.type != EventType.STATE_ROOM_MEMBER) { + return false + } + val userId = event.stateKey ?: return false + val roomMember = event.content.toModel<RoomMemberContent>() + return handle(realm, roomId, userId, roomMember) + } + + fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean { + if (roomMember == null) { + return false + } + val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + realm.insertOrUpdate(roomMemberEntity) + if (roomMember.membership.isActive()) { + val userEntity = UserEntityFactory.create(userId, roomMember) + realm.insertOrUpdate(userEntity) + } + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ef2d973c093b558be2b72f26131474d5674a5dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import io.realm.RealmQuery + +/** + * This class is an helper around STATE_ROOM_MEMBER events. + * It allows to get the live membership of a user. + */ + +internal class RoomMemberHelper(private val realm: Realm, + private val roomId: String +) { + + private val roomSummary: RoomSummaryEntity? by lazy { + RoomSummaryEntity.where(realm, roomId).findFirst() + } + + fun getLastStateEvent(userId: String): EventEntity? { + return CurrentStateEventEntity.getOrNull(realm, roomId, userId, EventType.STATE_ROOM_MEMBER)?.root + } + + fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? { + return RoomMemberSummaryEntity + .where(realm, roomId, userId) + .findFirst() + } + + fun isUniqueDisplayName(displayName: String?): Boolean { + if (displayName.isNullOrEmpty()) { + return true + } + return RoomMemberSummaryEntity.where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, displayName) + .findAll() + .size == 1 + } + + fun queryRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> { + return RoomMemberSummaryEntity.where(realm, roomId) + } + + fun queryJoinedRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> { + return queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + + fun queryInvitedRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> { + return queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + } + + fun queryActiveRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> { + return queryRoomMembersEvent() + .beginGroup() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .or() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .endGroup() + } + + fun getNumberOfJoinedMembers(): Int { + return roomSummary?.joinedMembersCount + ?: queryJoinedRoomMembersEvent().findAll().size + } + + fun getNumberOfInvitedMembers(): Int { + return roomSummary?.invitedMembersCount + ?: queryInvitedRoomMembersEvent().findAll().size + } + + fun getNumberOfMembers(): Int { + return getNumberOfJoinedMembers() + getNumberOfInvitedMembers() + } + + /** + * Return all the roomMembers ids which are joined or invited to the room + * + * @return a roomMember id list of joined or invited members. + */ + fun getActiveRoomMemberIds(): List<String> { + return queryActiveRoomMembersEvent().findAll().map { it.userId } + } + + /** + * Return all the roomMembers ids which are joined to the room + * + * @return a roomMember id list of joined members. + */ + fun getJoinedRoomMemberIds(): List<String> { + return queryJoinedRoomMembersEvent().findAll().map { it.userId } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8ac0983ebba63f6fc464c899da4f8d50f0c12c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RoomMembersResponse( + @Json(name = "chunk") val roomMemberEvents: List<Event> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..948bab39fbea0f5f7e5fd6bf5b77f926c0272d79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.admin + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface MembershipAdminTask : Task<MembershipAdminTask.Params, Unit> { + + enum class Type { + BAN, + UNBAN, + KICK + } + + data class Params( + val type: Type, + val roomId: String, + val userId: String, + val reason: String? + ) +} + +internal class DefaultMembershipAdminTask @Inject constructor(private val roomAPI: RoomAPI) : MembershipAdminTask { + + override suspend fun execute(params: MembershipAdminTask.Params) { + val userIdAndReason = UserIdAndReason(params.userId, params.reason) + executeRequest<Unit>(null) { + apiCall = when (params.type) { + MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt new file mode 100644 index 0000000000000000000000000000000000000000..4589b5e4bc1daf97878ff949954641b54dcb678a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.membership.admin + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserIdAndReason( + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..6fad0b10b732d427ee196dd0be3c4db239d356e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InviteBody( + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..4b9935d2faf2d176ec389834b5ddedaf4b9c09ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface InviteTask : Task<InviteTask.Params, Unit> { + data class Params( + val roomId: String, + val userId: String, + val reason: String? + ) +} + +internal class DefaultInviteTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : InviteTask { + + override suspend fun execute(params: InviteTask.Params) { + return executeRequest(eventBus) { + val body = InviteBody(params.userId, params.reason) + apiCall = roomAPI.invite(params.roomId, body) + isRetryable = true + maxRetryCount = 3 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3e2efcde3a46ffcb939f12c601280551457ae1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.task.Task +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> { + data class Params( + val roomIdOrAlias: String, + val reason: String?, + val viaServers: List<String> = emptyList() + ) +} + +internal class DefaultJoinRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val eventBus: EventBus +) : JoinRoomTask { + + override suspend fun execute(params: JoinRoomTask.Params) { + roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining) + val joinRoomResponse = try { + executeRequest<JoinRoomResponse>(eventBus) { + apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) + } + } catch (failure: Throwable) { + roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure)) + throw failure + } + // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val roomId = joinRoomResponse.roomId + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { + throw JoinRoomFailure.JoinedWithTimeout + } + setReadMarkers(roomId) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true) + readMarkersTask.execute(setReadMarkerParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a3e6c0aaade0956af8bf5b72dc52a71ac9cc37d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.leaving + +import org.matrix.android.sdk.api.query.QueryStringValue +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.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> { + data class Params( + val roomId: String, + val reason: String? + ) +} + +internal class DefaultLeaveRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus, + private val stateEventDataSource: StateEventDataSource, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource +) : LeaveRoomTask { + + override suspend fun execute(params: LeaveRoomTask.Params) { + leaveRoom(params.roomId, params.reason) + } + + private suspend fun leaveRoom(roomId: String, reason: String?) { + val roomSummary = roomSummaryDataSource.getRoomSummary(roomId) + if (roomSummary?.membership?.isActive() == false) { + Timber.v("Room $roomId is not joined so can't be left") + return + } + roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving) + val roomCreateStateEvent = stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = EventType.STATE_ROOM_CREATE, + stateKey = QueryStringValue.NoCondition + ) + // Server is not cleaning predecessor rooms, so we also try to left them + val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId + if (predecessorRoomId != null) { + leaveRoom(predecessorRoomId, reason) + } + try { + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.leave(roomId, mapOf("reason" to reason)) + } + } catch (failure: Throwable) { + roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure)) + throw failure + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b18e44360dda98fb8c69990abd3be504b92d97a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.threepid + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> { + data class Params( + val roomId: String, + val threePid: ThreePid + ) +} + +internal class DefaultInviteThreePidTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus, + private val identityStore: IdentityStore, + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : InviteThreePidTask { + + override suspend fun execute(params: InviteThreePidTask.Params) { + ensureIdentityTokenTask.execute(Unit) + + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest(eventBus) { + val body = ThreePidInviteBody( + id_server = identityServerUrlWithoutProtocol, + id_access_token = identityServerAccessToken, + medium = params.threePid.toMedium(), + address = params.threePid.value + ) + apiCall = roomAPI.invite3pid(params.roomId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..93b5c577fcc5706f2fc075a4fa32f6aa5b90ab51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.membership.threepid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThreePidInviteBody( + /** + * Required. The hostname+port of the identity server which should be used for third party identifier lookups. + */ + @Json(name = "id_server") val id_server: String, + /** + * Required. An access token previously registered with the identity server. Servers can treat this as optional + * to distinguish between r0.5-compatible clients and this specification version. + */ + @Json(name = "id_access_token") val id_access_token: String, + /** + * Required. The kind of address being passed in the address field, for example email. + */ + @Json(name = "medium") val medium: String, + /** + * Required. The invitee's third party identifier. + */ + @Json(name = "address") val address: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt new file mode 100644 index 0000000000000000000000000000000000000000..93e2881c1324a2d1d3045b0171c2dbd73e11e4f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, + private val setRoomNotificationStateTask: SetRoomNotificationStateTask, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RoomPushRuleService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RoomPushRuleService + } + + override fun getLiveRoomNotificationState(): LiveData<RoomNotificationState> { + return Transformations.map(getPushRuleForRoom()) { + it?.toRoomNotificationState() ?: RoomNotificationState.ALL_MESSAGES + } + } + + override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable { + return setRoomNotificationStateTask + .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + private fun getPushRuleForRoom(): LiveData<RoomPushRule?> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + PushRuleEntity.where(realm, scope = RuleScope.GLOBAL, ruleId = roomId) + }, + { result -> + result.toRoomPushRule() + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7c77193428e96949d240644173078739e3a3c4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule + +internal data class RoomPushRule( + val kind: RuleKind, + val rule: PushRule +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a19a40602e6f8ee8c2aca25cf01d6fd190e7780 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.toJson +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRuleEntity + +internal fun PushRuleEntity.toRoomPushRule(): RoomPushRule? { + val kind = parent?.firstOrNull()?.kind + val pushRule = when (kind) { + RuleSetKey.OVERRIDE -> { + PushRulesMapper.map(this) + } + RuleSetKey.ROOM -> { + PushRulesMapper.mapRoomRule(this) + } + else -> null + } + return if (pushRule == null || kind == null) { + null + } else { + RoomPushRule(kind, pushRule) + } +} + +internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? { + return when { + this == RoomNotificationState.ALL_MESSAGES -> null + this == RoomNotificationState.ALL_MESSAGES_NOISY -> { + val rule = PushRule( + actions = listOf(Action.Notify, Action.Sound()).toJson(), + enabled = true, + ruleId = roomId + ) + return RoomPushRule(RuleSetKey.ROOM, rule) + } + else -> { + val condition = PushCondition( + kind = Condition.Kind.EventMatch.value, + key = "room_id", + pattern = roomId + ) + val rule = PushRule( + actions = listOf(Action.DoNotNotify).toJson(), + enabled = true, + ruleId = roomId, + conditions = listOf(condition) + ) + val kind = if (this == RoomNotificationState.MUTE) { + RuleSetKey.OVERRIDE + } else { + RuleSetKey.ROOM + } + return RoomPushRule(kind, rule) + } + } +} + +internal fun RoomPushRule.toRoomNotificationState(): RoomNotificationState { + return if (rule.enabled) { + val actions = rule.getActions() + if (actions.contains(Action.DoNotNotify)) { + if (kind == RuleSetKey.OVERRIDE) { + RoomNotificationState.MUTE + } else { + RoomNotificationState.MENTIONS_ONLY + } + } else if (actions.contains(Action.Notify)) { + val hasSoundAction = actions.find { + it is Action.Sound + } != null + if (hasSoundAction) { + RoomNotificationState.ALL_MESSAGES_NOISY + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a1b8d2a65f76ccca8f93500d58f356844eeefdb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.pushers.AddPushRuleTask +import org.matrix.android.sdk.internal.session.pushers.RemovePushRuleTask +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface SetRoomNotificationStateTask : Task<SetRoomNotificationStateTask.Params, Unit> { + data class Params( + val roomId: String, + val roomNotificationState: RoomNotificationState + ) +} + +internal class DefaultSetRoomNotificationStateTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val removePushRuleTask: RemovePushRuleTask, + private val addPushRuleTask: AddPushRuleTask) + : SetRoomNotificationStateTask { + + override suspend fun execute(params: SetRoomNotificationStateTask.Params) { + val currentRoomPushRule = Realm.getInstance(monarchy.realmConfiguration).use { + PushRuleEntity.where(it, scope = RuleScope.GLOBAL, ruleId = params.roomId).findFirst()?.toRoomPushRule() + } + if (currentRoomPushRule != null) { + removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule)) + } + val newRoomPushRule = params.roomNotificationState.toRoomPushRule(params.roomId) + if (newRoomPushRule != null) { + addPushRuleTask.execute(AddPushRuleTask.Params(newRoomPushRule.kind, newRoomPushRule.rule)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ff7ae69bb697131910b1ae9444b2f97b597e9ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.prune + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ +internal class RedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.REDACTION + } + + override suspend fun process(realm: Realm, event: Event) { + pruneEvent(realm, event) + } + + private fun pruneEvent(realm: Realm, redactionEvent: Event) { + if (redactionEvent.redacts.isNullOrBlank()) { + return + } + + // Check that we know this event + EventEntity.where(realm, eventId = redactionEvent.eventId ?: "").findFirst() ?: return + + val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "") + Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") + + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() + ?: return + + val typeToPrune = eventToPrune.type + val stateKey = eventToPrune.stateKey + val allowedKeys = computeAllowedKeys(typeToPrune) + if (allowedKeys.isNotEmpty()) { + val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } + eventToPrune.content = ContentMapper.map(prunedContent) + } else { + when (typeToPrune) { + EventType.ENCRYPTED, + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + // was this event a m.replace +// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() +// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { +// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) +// } + + val modified = unsignedData.copy(redactedEvent = redactionEvent) + eventToPrune.content = ContentMapper.map(emptyMap()) + eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + eventToPrune.decryptionResultJson = null + eventToPrune.decryptionErrorCode = null + } +// EventType.REACTION -> { +// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) +// } + } + } + if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { + TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId).forEach { + it.senderName = null + it.isUniqueDisplayName = false + it.senderAvatar = null + } + } + } + + private fun computeAllowedKeys(type: String): List<String> { + // Add filtered content, allowed keys in content depends on the event type + return when (type) { + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + "users_default", + "events", + "events_default", + "state_default", + "ban", + "kick", + "redact", + "invite") + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5520972b00bf10154bf2281347f9372848e35d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultReadService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + @UserId private val userId: String +) : ReadService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReadService + } + + override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback<Unit>) { + val taskParams = SetReadMarkersTask.Params( + roomId = roomId, + forceReadMarker = params.forceReadMarker(), + forceReadReceipt = params.forceReadReceipt() + ) + setReadMarkersTask + .configureWith(taskParams) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun isEventRead(eventId: String): Boolean { + return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) + } + + override fun getReadMarkerLive(): LiveData<Optional<String>> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadMarkerEntity.where(it, roomId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() + } + } + + override fun getMyReadReceiptLive(): LiveData<Optional<String>> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() + } + } + + override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptsSummaryEntity.where(it, eventId) }, + { readReceiptsSummaryMapper.map(it) } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().orEmpty() + } + } + + private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean { + return this == ReadService.MarkAsReadParams.READ_MARKER || this == ReadService.MarkAsReadParams.BOTH + } + + private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean { + return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2b216d625aae3d67323c30a7ed13f57d2655089 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FullyReadContent( + @Json(name = "event_id") val eventId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b06b83aac3ec4b144d4fe387412ec6cb06aac18e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface MarkAllRoomsReadTask : Task<MarkAllRoomsReadTask.Params, Unit> { + data class Params( + val roomIds: List<String> + ) +} + +internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask { + + override suspend fun execute(params: MarkAllRoomsReadTask.Params) { + params.roomIds.forEach { roomId -> + readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..f750735bbb22c5c1bcf73b0b7a0dcb7c010447e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.isReadMarkerMoreRecent +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject +import kotlin.collections.set + +internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> { + + data class Params( + val roomId: String, + val fullyReadEventId: String? = null, + val readReceiptEventId: String? = null, + val forceReadReceipt: Boolean = false, + val forceReadMarker: Boolean = false + ) +} + +private const val READ_MARKER = "m.fully_read" +private const val READ_RECEIPT = "m.read" + +internal class DefaultSetReadMarkersTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val readReceiptHandler: ReadReceiptHandler, + @UserId private val userId: String, + private val eventBus: EventBus +) : SetReadMarkersTask { + + override suspend fun execute(params: SetReadMarkersTask.Params) { + val markers = HashMap<String, String>() + Timber.v("Execute set read marker with params: $params") + val latestSyncedEventId = latestSyncedEventId(params.roomId) + val fullyReadEventId = if (params.forceReadMarker) { + latestSyncedEventId + } else { + params.fullyReadEventId + } + val readReceiptEventId = if (params.forceReadReceipt) { + latestSyncedEventId + } else { + params.readReceiptEventId + } + if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) { + if (LocalEcho.isLocalEchoId(fullyReadEventId)) { + Timber.w("Can't set read marker for local event $fullyReadEventId") + } else { + markers[READ_MARKER] = fullyReadEventId + } + } + if (readReceiptEventId != null + && !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) { + if (LocalEcho.isLocalEchoId(readReceiptEventId)) { + Timber.w("Can't set read receipt for local event $readReceiptEventId") + } else { + markers[READ_RECEIPT] = readReceiptEventId + } + } + + val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId + if (markers.isNotEmpty() || shouldUpdateRoomSummary) { + updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) + } + if (markers.isNotEmpty()) { + executeRequest<Unit>(eventBus) { + isRetryable = true + apiCall = roomAPI.sendReadMarker(params.roomId, markers) + } + } + } + + private fun latestSyncedEventId(roomId: String): String? = + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId + } + + private suspend fun updateDatabase(roomId: String, markers: HashMap<String, String>, shouldUpdateRoomSummary: Boolean) { + monarchy.awaitTransaction { realm -> + val readMarkerId = markers[READ_MARKER] + val readReceiptId = markers[READ_RECEIPT] + if (readMarkerId != null) { + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) + } + if (readReceiptId != null) { + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) + readReceiptHandler.handle(realm, roomId, readReceiptContent, false) + } + if (shouldUpdateRoomSummary) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: return@awaitTransaction + roomSummary.notificationCount = 0 + roomSummary.highlightCount = 0 + roomSummary.hasUnreadMessages = false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..458d98bd52238802767430ce13249cb4b01e714f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.work.OneTimeWorkRequest +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker +import org.matrix.android.sdk.internal.session.room.send.SendEventWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.fetchCopyMap +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import timber.log.Timber + +internal class DefaultRelationService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionId private val sessionId: String, + private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon, + private val eventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, + private val timelineEventMapper: TimelineEventMapper, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RelationService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RelationService + } + + override fun sendReaction(targetEventId: String, reaction: String): Cancelable { + return if (monarchy + .fetchCopyMap( + { realm -> + TimelineEventEntity.where(realm, roomId, targetEventId).findFirst() + }, + { entity, _ -> + timelineEventMapper.map(entity) + }) + ?.annotations + ?.reactionsSummary + .orEmpty() + .none { it.addedByMe && it.key == reaction }) { + val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) + .also { saveLocalEcho(it) } + val sendRelationWork = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork) + } else { + Timber.w("Reaction already added") + NoOpCancellable + } + } + + override fun undoReaction(targetEventId: String, reaction: String): Cancelable { + val params = FindReactionEventForUndoTask.Params( + roomId, + targetEventId, + reaction + ) + // TODO We should avoid using MatrixCallback internally + val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> { + override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + // TODO? + } + data.redactEventId?.let { toRedact -> + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null) + .also { saveLocalEcho(it) } + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + + timeLineSendEventWorkCommon.postWork(roomId, redactWork) + } + } + } + return findReactionEventForUndoTask + .configureWith(params) { + this.retryCount = Int.MAX_VALUE + this.callback = callback + } + .executeBy(taskExecutor) + } + + // TODO duplicate with send service? + private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { + val sendContentWorkerParams = RedactEventWorker.Params( + sessionId, + localEvent.eventId!!, + roomId, + eventId, + reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return timeLineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true) + } + + override fun editTextMessage(targetEventId: String, + msgType: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + compatibilityBodyText: String): Cancelable { + val event = eventFactory + .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) + .also { saveLocalEcho(it) } + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + override fun editReply(replyToEdit: TimelineEvent, + originalTimelineEvent: TimelineEvent, + newBodyText: String, + compatibilityBodyText: String): Cancelable { + val event = eventFactory + .createReplaceTextOfReply(roomId, + replyToEdit, + originalTimelineEvent, + newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + .also { saveLocalEcho(it) } + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { + val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) + fetchEditHistoryTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { + val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) + ?.also { saveLocalEcho(it) } + ?: return null + + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, event, keepKeys) + val sendWorkData = WorkerParamsFactory.toData(params) + return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true) + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) + } + + override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { + return monarchy.fetchCopyMap( + { EventAnnotationsSummaryEntity.where(it, eventId).findFirst() }, + { entity, _ -> + entity.asDomain() + } + ) + } + + override fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> { + val liveData = monarchy.findAllMappedWithChanges( + { EventAnnotationsSummaryEntity.where(it, eventId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + /** + * Saves the event in database as a local echo. + * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. + * The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with + * the same transaction id is received (in unsigned data) + */ + private fun saveLocalEcho(event: Event) { + eventFactory.createLocalEcho(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..28cdd9a72bdcb4d04ca6777edcecd2f3497f51a2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> { + + data class Params( + val roomId: String, + val isRoomEncrypted: Boolean, + val eventId: String + ) +} + +internal class DefaultFetchEditHistoryTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : FetchEditHistoryTask { + + override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> { + val response = executeRequest<RelationsResponse>(eventBus) { + apiCall = roomAPI.getRelations(params.roomId, + params.eventId, + RelationType.REPLACE, + if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) + } + + val events = response.chunks.toMutableList() + response.originalEvent?.let { events.add(it) } + return events + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..86fe75d9ed55cf762b4a4e851169ec2336124a29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoTask.Params, FindReactionEventForUndoTask.Result> { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String + ) + + data class Result( + val redactEventId: String? + ) +} + +internal class DefaultFindReactionEventForUndoTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String) : FindReactionEventForUndoTask { + + override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result { + val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + getReactionToRedact(realm, params.reaction, params.eventId)?.eventId + } + return FindReactionEventForUndoTask.Result(eventId) + } + + private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? { + val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null + + val rase = summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() ?: return null + + // want to find the event originated by me! + return rase.sourceEvents + .asSequence() + .mapNotNull { + // find source event + EventEntity.where(realm, it).findFirst() + } + .firstOrNull { eventEntity -> + // is it mine? + eventEntity.sender == userId + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..24fcf89bde45cfee3611e2b11d7ee567c9702829 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RelationsResponse( + @Json(name = "chunk") val chunks: List<Event>, + @Json(name = "original_event") val originalEvent: Event?, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc72c3b96bcd5a37cf7da1c47fc908be5598d3d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +// TODO This is not used. Delete? +internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val roomId: String, + val event: Event, + val relationType: String? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.failure() + } + val relationContent = localEvent.content.toModel<ReactionContent>() + ?: return Result.failure() + val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() + val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType + ?: return Result.failure() + return try { + sendRelation(params.roomId, relationType, relatedEventId, localEvent) + Result.success() + } catch (exception: Throwable) { + when (exception) { + is Failure.NetworkConnection -> Result.retry() + else -> { + // TODO mark as failed to send? + // always return success, or the chain will be stuck for ever! + Result.success() + } + } + } + } + + private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { + executeRequest<SendResponse>(eventBus) { + apiCall = roomAPI.sendRelation( + roomId = roomId, + parent_id = relatedEventId, + relationType = relationType, + eventType = localEvent.type, + content = localEvent.content + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d235cdba3b4b90ce69f6d42eab3490cb7ba2d939 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params, UpdateQuickReactionTask.Result> { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val oppositeReaction: String + ) + + data class Result( + val reactionToAdd: String?, + val reactionToRedact: List<String> + ) +} + +internal class DefaultUpdateQuickReactionTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String) : UpdateQuickReactionTask { + + override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result { + var res: Pair<String?, List<String>?>? = null + monarchy.doWithRealm { realm -> + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId) + } + return UpdateQuickReactionTask.Result(res?.first, res?.second.orEmpty()) + } + + private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> { + // the emoji reaction has been selected, we need to check if we have reacted it or not + val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: return Pair(reaction, null) + + // Ok there is already reactions on this event, have we reacted to it + val aggregationForReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() + val aggregationForOppositeReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction) + .findFirst() + + if (aggregationForReaction == null || !aggregationForReaction.addedByMe) { + // i haven't yet reacted to it, so need to add it, but do I need to redact the opposite? + val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { + // find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == userId) entity.eventId else null + } + return Pair(reaction, toRedact) + } else { + // I already added it, so i need to undo it (like a toggle) + // find all m.redaction coming from me to readact them + val toRedact = aggregationForReaction.sourceEvents.mapNotNull { + // find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == userId) entity.eventId else null + } + return Pair(null, toRedact) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..1117ed1c29f909330c7fe83dd49b7ae76c78fdae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val reportContentTask: ReportContentTask +) : ReportingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReportingService + } + + override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable { + val params = ReportContentTask.Params(roomId, eventId, score, reason) + + return reportContentTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd9f09f4fd86bc293c2f3d0bb27e30f2404c0d0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ReportContentBody( + /** + * Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive. + */ + @Json(name = "score") val score: Int, + + /** + * Required. The reason the content is being reported. May be blank. + */ + @Json(name = "reason") val reason: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b13c21fa2ac54f9bee02a3a11a29cd3f839e28c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface ReportContentTask : Task<ReportContentTask.Params, Unit> { + data class Params( + val roomId: String, + val eventId: String, + val score: Int, + val reason: String + ) +} + +internal class DefaultReportContentTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ReportContentTask { + + override suspend fun execute(params: ReportContentTask.Params) { + return executeRequest(eventBus) { + apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6fa6775ee1a6100b09cdead5f70fa67514cdcb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.CancelableBag +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.startChain +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +private const val UPLOAD_WORK = "UPLOAD_WORK" + +internal class DefaultSendService @AssistedInject constructor( + @Assisted private val roomId: String, + private val workManagerProvider: WorkManagerProvider, + private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon, + @SessionId private val sessionId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository, + private val roomEventSender: RoomEventSender +) : SendService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): SendService + } + + private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() + + override fun sendEvent(eventType: String, content: JsonDict?): Cancelable { + return localEchoEventFactory.createEvent(roomId, eventType, content) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + // For test only + private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable { + return CancelableBag().apply { + // Send the event several times + repeat(times) { i -> + localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + .also { add(it) } + } + } + } + + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendPoll(question: String, options: List<OptionItem>): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, question, options) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { + return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendMedias(attachments: List<ContentAttachmentData>, + compressBeforeSending: Boolean, + roomIds: Set<String>): Cancelable { + return attachments.mapTo(CancelableBag()) { + sendMedia(it, compressBeforeSending, roomIds) + } + } + + override fun redactEvent(event: Event, reason: String?): Cancelable { + // TODO manage media/attachements? + return createRedactEventWork(event, reason) + .let { timelineSendEventWorkCommon.postWork(roomId, it) } + } + + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return sendEvent(localEcho.root) + } + return null + } + + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { + // TODO this need a refactoring of attachement sending +// val clearContent = localEcho.root.getClearContent() +// val messageContent = clearContent?.toModel<MessageContent>() ?: return null +// when (messageContent.type) { +// MessageType.MSGTYPE_IMAGE -> { +// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null +// val url = imageContent.url ?: return null +// if (url.startsWith("mxc://")) { +// //TODO +// } else { +// //The image has not yet been sent +// val attachmentData = ContentAttachmentData( +// size = imageContent.info!!.size.toLong(), +// mimeType = imageContent.info.mimeType!!, +// width = imageContent.info.width.toLong(), +// height = imageContent.info.height.toLong(), +// name = imageContent.body, +// path = imageContent.url, +// type = ContentAttachmentData.Type.IMAGE +// ) +// monarchy.runTransactionSync { +// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { +// it.sendState = SendState.UNSENT +// } +// } +// return internalSendMedia(localEcho.root,attachmentData) +// } +// } +// } + return null + } + return null + } + + override fun deleteFailedEcho(localEcho: TimelineEvent) { + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, localEcho) + } + } + + override fun clearSendingQueue() { + timelineSendEventWorkCommon.cancelAllWorks(roomId) + workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK)) + + // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied + workManagerProvider.matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>() + .build().let { + timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE) + + // need to clear also image sending queue + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) + .enqueue() + } + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) + } + } + + override fun resendAllFailedMessages() { + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) + } + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + } + } + + override fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set<String>): Cancelable { + // Create an event with the media file path + // Ensure current roomId is included in the set + val allRoomIds = (roomIds + roomId).toList() + + // Create local echo for each room + val allLocalEchoes = allRoomIds.map { + localEchoEventFactory.createMediaEvent(it, attachment).also { event -> + createLocalEcho(event) + } + } + return internalSendMedia(allLocalEchoes, attachment, compressBeforeSending) + } + + /** + * We use the roomId of the local echo event + */ + private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { + val cancelableBag = CancelableBag() + + allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } + .apply { + keys.forEach { isRoomEncrypted -> + // Should never be empty + val localEchoes = get(isRoomEncrypted).orEmpty() + val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending) + + val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted) + + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(dispatcherWork) + .enqueue() + .also { operation -> + operation.result.addListener(Runnable { + if (operation.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (operation.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) + } + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id)) + } + } + + return cancelableBag + } + + private fun sendEvent(event: Event): Cancelable { + return roomEventSender.sendEvent(event) + } + + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) + } + + private fun buildWorkName(identifier: String): String { + return "${roomId}_$identifier" + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + return EncryptEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { + workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(it) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + } + + private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { + return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + .also { createLocalEcho(it) } + .let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) } + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork<RedactEventWorker>(it, true) } + } + + private fun createUploadMediaWork(allLocalEchos: List<Event>, + attachment: ContentAttachmentData, + isRoomEncrypted: Boolean, + compressBeforeSending: Boolean): OneTimeWorkRequest { + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) + val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(true) + .setInputData(uploadWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createMultipleEventDispatcherWork(isRoomEncrypted: Boolean): OneTimeWorkRequest { + // the list of events will be replaced by the result of the media upload work + val params = MultipleEventSendingDispatcherWorker.Params(sessionId, emptyList(), isRoomEncrypted) + val workData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder<MultipleEventSendingDispatcherWorker>() + // No constraint + // .setConstraints(WorkManagerProvider.workConstraints) + .startChain(false) + .setInputData(workData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..d23835e83883b93ecd8c3bc00393ba93d266c42e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.util.awaitCallback +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : Always [SendEventWorker] + */ +internal class EncryptEventWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val event: Event, + /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ + val keepKeys: List<String>? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var crypto: CryptoService + @Inject lateinit var localEchoRepository: LocalEchoRepository + + override suspend fun doWork(): Result { + Timber.v("Start Encrypt work") + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + Timber.v("Start Encrypt work for event ${params.event.eventId}") + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.success() + } + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + + var error: Throwable? = null + var result: MXEncryptEventContentResult? = null + try { + result = awaitCallback { + crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it) + } + } catch (throwable: Throwable) { + error = throwable + } + if (result != null) { + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + val encryptedEvent = localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + // Better handling of local echo, to avoid decrypting transition on remote echo + // Should I only do it for text messages? + if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + val decryptionLocalEcho = MXEventDecryptionResult( + clearEvent = Event( + type = localEvent.type, + content = localEvent.content, + roomId = localEvent.roomId + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, + claimedEd25519Key = crypto.getMyDevice().fingerprint() + ) + localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) + } + + val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) + return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) + } else { + val sendState = when (error) { + is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES + else -> SendState.UNDELIVERED + } + localEchoRepository.updateSendState(localEvent.eventId, sendState) + // always return success, or the chain will be stuck for ever! + val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage + ?: "Error") + return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe6daad3b077d066352cc9df906280a41a24aa4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -0,0 +1,493 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import androidx.exifinterface.media.ExifInterface +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.permalinks.PermalinkFactory +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.AudioInfo +import org.matrix.android.sdk.api.session.room.model.message.FileInfo +import org.matrix.android.sdk.api.session.room.model.message.ImageInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo +import org.matrix.android.sdk.api.session.room.model.message.VideoInfo +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor +import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.StringProvider +import javax.inject.Inject + +/** + * Creates local echo of events for room events. + * A local echo is an event that is persisted even if not yet sent to the server, + * in an optimistic way (as if the server as responded immediately). Local echo are using a local id, + * (the transaction ID), this id is used when receiving an event from a sync to check if this event + * is matching an existing local echo. + * + * The transactionId is used as loc + */ +internal class LocalEchoEventFactory @Inject constructor( + private val context: Context, + @UserId private val userId: String, + private val stringProvider: StringProvider, + private val markdownParser: MarkdownParser, + private val textPillsUtils: TextPillsUtils, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository +) { + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { + return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) + } + val content = MessageTextContent(msgType = msgType, body = text.toString()) + return createMessageEvent(roomId, content) + } + + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { + if (autoMarkdown) { + val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() + return markdownParser.parse(source) + } else { + // Try to detect pills + textPillsUtils.processSpecialSpansToHtml(text)?.let { + return TextContent(text.toString(), it) + } + } + + return TextContent(text.toString()) + } + + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { + return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) + } + + fun createReplaceTextEvent(roomId: String, + targetEventId: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + msgType: String, + compatibilityText: String): Event { + return createMessageEvent(roomId, + MessageTextContent( + msgType = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = createTextContent(newBodyText, newBodyAutoMarkdown) + .toMessageTextContent(msgType) + .toContent() + )) + } + + fun createOptionsReplyEvent(roomId: String, + pollEventId: String, + optionIndex: Int, + optionLabel: String): Event { + return createMessageEvent(roomId, + MessagePollResponseContent( + body = optionLabel, + relatesTo = RelationDefaultContent( + type = RelationType.RESPONSE, + option = optionIndex, + eventId = pollEventId) + + )) + } + + fun createPollEvent(roomId: String, + question: String, + options: List<OptionItem>): Event { + val compatLabel = buildString { + append("[Poll] ") + append(question) + options.forEach { + append("\n") + append(it.value) + } + } + return createMessageEvent( + roomId, + MessageOptionsContent( + body = compatLabel, + label = question, + optionType = OPTION_TYPE_POLL, + options = options.toList() + ) + ) + } + + fun createReplaceTextOfReply(roomId: String, + eventReplaced: TimelineEvent, + originalEvent: TimelineEvent, + newBodyText: String, + newBodyAutoMarkdown: Boolean, + msgType: String, + compatibilityText: String): Event { + val permalink = PermalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "") + val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) } + ?: "" + + val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) + val replyFormatted = REPLY_PATTERN.format( + permalink, + userLink, + originalEvent.senderInfo.disambiguatedDisplayName, + // Remove inner mx_reply tags if any + body.takeFormatted().replace(MX_REPLY_REGEX, ""), + createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() + ) + // + // > <@alice:example.org> This is the original body + // + val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText) + + return createMessageEvent(roomId, + MessageTextContent( + msgType = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId), + newContent = MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = replyFallback, + formattedBody = replyFormatted + ) + .toContent() + )) + } + + fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { + return when (attachment.type) { + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) + ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + } + } + + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + val content = ReactionContent( + ReactionInfo( + RelationType.ANNOTATION, + targetEventId, + reaction + ) + ) + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.REACTION, + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId)) + } + + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { + var width = attachment.width + var height = attachment.height + + when (attachment.exifOrientation) { + ExifInterface.ORIENTATION_ROTATE_90, + ExifInterface.ORIENTATION_TRANSVERSE, + ExifInterface.ORIENTATION_ROTATE_270, + ExifInterface.ORIENTATION_TRANSPOSE -> { + val tmp = width + width = height + height = tmp + } + } + + val content = MessageImageContent( + msgType = MessageType.MSGTYPE_IMAGE, + body = attachment.name ?: "image", + info = ImageInfo( + mimeType = attachment.getSafeMimeType(), + width = width?.toInt() ?: 0, + height = height?.toInt() ?: 0, + size = attachment.size.toInt() + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + val mediaDataRetriever = MediaMetadataRetriever() + mediaDataRetriever.setDataSource(context, attachment.queryUri) + + // Use frame to calculate height and width as we are sure to get the right ones + val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime + val height = firstFrame?.height ?: 0 + val width = firstFrame?.width ?: 0 + mediaDataRetriever.release() + + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { + ThumbnailInfo( + width = it.width, + height = it.height, + size = it.size, + mimeType = it.mimeType + ) + } + val content = MessageVideoContent( + msgType = MessageType.MSGTYPE_VIDEO, + body = attachment.name ?: "video", + videoInfo = VideoInfo( + mimeType = attachment.getSafeMimeType(), + width = width, + height = height, + size = attachment.size, + duration = attachment.duration?.toInt() ?: 0, + // Glide will be able to use the local path and extract a thumbnail. + thumbnailUrl = attachment.queryUri.toString(), + thumbnailInfo = thumbnailInfo + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageAudioContent( + msgType = MessageType.MSGTYPE_AUDIO, + body = attachment.name ?: "audio", + audioInfo = AudioInfo( + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, + size = attachment.size + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageFileContent( + msgType = MessageType.MSGTYPE_FILE, + body = attachment.name ?: "file", + info = FileInfo( + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, + size = attachment.size + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { + return createEvent(roomId, EventType.MESSAGE, content.toContent()) + } + + fun createEvent(roomId: String, type: String, content: Content?): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List<String>): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.MESSAGE, + content = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = fromDevice, + toUserId = toUserId, + timestamp = System.currentTimeMillis(), + methods = methods + ).toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + private fun dummyOriginServerTs(): Long { + return System.currentTimeMillis() + } + + fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? { + // Fallbacks and event representation + // TODO Add error/warning logs when any of this is null + val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null + val userId = eventReplied.root.senderId ?: return null + val userLink = PermalinkFactory.createPermalink(userId) ?: return null + + val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) + val replyFormatted = REPLY_PATTERN.format( + permalink, + userLink, + userId, + // Remove inner mx_reply tags if any + body.takeFormatted().replace(MX_REPLY_REGEX, ""), + createTextContent(replyText, autoMarkdown).takeFormatted() + ) + // + // > <@alice:example.org> This is the original body + // + val replyFallback = buildReplyFallback(body, userId, replyText.toString()) + + val eventId = eventReplied.root.eventId ?: return null + val content = MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = replyFallback, + formattedBody = replyFormatted, + relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) + ) + return createMessageEvent(roomId, content) + } + + private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { + return buildString { + append("> <") + append(originalSenderId) + append(">") + + val lines = body.text.split("\n") + lines.forEachIndexed { index, s -> + if (index == 0) { + append(" $s") + } else { + append("\n> $s") + } + } + append("\n\n") + append(newBodyText) + } + } + + /** + * Returns a TextContent used for the fallback event representation in a reply message. + * In case of an edit of a reply the last content is not + * himself a reply, but it will contain the fallbacks, so we have to trim them. + */ + private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent { + when (content?.msgType) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE -> { + var formattedText: String? = null + if (content is MessageContentWithFormattedBody) { + formattedText = content.matrixFormattedBody + } + return if (isReply) { + TextContent(content.body, formattedText).removeInReplyFallbacks() + } else { + TextContent(content.body, formattedText) + } + } + MessageType.MSGTYPE_FILE -> return TextContent("sent a file.") + MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") + MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") + MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") + else -> return TextContent(content?.body ?: "") + } + } + + /* + * { + "content": { + "reason": "Spamming" + }, + "event_id": "$143273582443PhrSn:domain.com", + "origin_server_ts": 1432735824653, + "redacts": "$fukweghifu23:localhost", + "room_id": "!jEsUZKDJdhlrceRyVU:domain.com", + "sender": "@example:domain.com", + "type": "m.room.redaction", + "unsigned": { + "age": 1234 + } + } + */ + fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.REDACTION, + redacts = eventId, + content = reason?.let { mapOf("reason" to it).toContent() }, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + fun createLocalEcho(event: Event) { + checkNotNull(event.roomId) { "Your event should have a roomId" } + localEchoRepository.createLocalEcho(event) + } + + companion object { + // <mx-reply> + // <blockquote> + // <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a> + // <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a> + // <br /> + // <!-- This is where the related event's HTML would be. --> + // </blockquote> + // </mx-reply> + // No whitespace because currently breaks temporary formatted text to Span + const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s""" + + // This is used to replace inner mx-reply tags + val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9859136ad96e38757f8a7df1b802d200f18f3d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.database.helper.nextId +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val eventBus: EventBus, + private val timelineEventMapper: TimelineEventMapper) { + + fun createLocalEcho(event: Event) { + val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") + val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event") + if (event.eventId == null) { + throw IllegalStateException("You should have set an eventId for your event") + } + val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val myUser = roomMemberHelper.getLastRoomMember(senderId) + val localId = TimelineEventEntity.nextId(realm) + TimelineEventEntity(localId).also { + it.root = eventEntity + it.eventId = event.eventId + it.roomId = roomId + it.senderName = myUser?.displayName + it.senderAvatar = myUser?.avatarUrl + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) + } + } + val timelineEvent = timelineEventMapper.map(timelineEventEntity) + eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) + monarchy.writeAsync { realm -> + val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { + this.insertType = EventInsertType.LOCAL_ECHO + } + realm.insert(eventInsertEntity) + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync + roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + fun updateSendState(eventId: String, sendState: SendState) { + Timber.v("Update local state of $eventId to ${sendState.name}") + monarchy.writeAsync { realm -> + val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() + if (sendingEventEntity != null) { + if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { + // If already synced, do not put as sent + } else { + sendingEventEntity.sendState = sendState + } + roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) + } + } + } + + fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { + monarchy.writeAsync { realm -> + val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() + if (sendingEventEntity != null) { + sendingEventEntity.type = EventType.ENCRYPTED + sendingEventEntity.content = ContentMapper.map(encryptedContent) + sendingEventEntity.setDecryptionResult(mxEventDecryptionResult) + } + } + } + + suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + suspend fun clearSendingQueue(roomId: String) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.IS_SENDING_STATES) + .forEach { + it.root?.sendState = SendState.UNSENT + } + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + suspend fun updateSendState(roomId: String, eventIds: List<String>, sendState: SendState) { + monarchy.awaitTransaction { realm -> + val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() + timelineEvents.forEach { + it.root?.sendState = sendState + } + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + fun getAllFailedEventsToResend(roomId: String): List<Event> { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .sortedByDescending { it.displayIndex } + .mapNotNull { it.root?.asDomain() } + .filter { event -> + when (event.getClearType()) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION -> { + val content = event.getClearContent().toModel<MessageContent>() + if (content != null) { + when (content.msgType) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_TEXT -> { + true + } + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO -> { + // need to resend the attachment + false + } + else -> { + Timber.e("Cannot resend message ${event.type} / ${content.msgType}") + false + } + } + } else { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + else -> { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..3390d9dc79ecd1e978ebe6aa46c4e3019392be47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import javax.inject.Inject + +/** + * This class convert a text to an html text + * This class is tested by [MarkdownParserTest]. + * If any change is required, please add a test covering the problem and make sure all the tests are still passing. + */ +internal class MarkdownParser @Inject constructor( + private val parser: Parser, + private val htmlRenderer: HtmlRenderer, + private val textContentRenderer: TextContentRenderer +) { + + private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex() + + fun parse(text: String): TextContent { + // If no special char are detected, just return plain text + if (text.contains(mdSpecialChars).not()) { + return TextContent(text) + } + + val document = parser.parse(text) + val htmlText = htmlRenderer.render(document) + + // Cleanup extra paragraph + val cleanHtmlText = if (htmlText.lastIndexOf("<p>") == 0) { + htmlText.removeSurrounding("<p>", "</p>\n") + } else { + htmlText + } + + return if (isFormattedTextPertinent(text, cleanHtmlText)) { + // According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes: + // The plain text version of the HTML should be provided in the body. + val plainText = textContentRenderer.render(document) + TextContent(plainText, cleanHtmlText.postTreatment()) + } else { + TextContent(text) + } + } + + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "<p>${text.trim()}</p>\n" + + /** + * The parser makes some mistakes, so deal with it here + */ + private fun String.postTreatment(): String { + return this + // Remove extra space before and after the content + .trim() + // There is no need to include new line in an html-like source + .replace("\n", "") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..73791e84127f4fc68297ec2b4489e9ad7d0dec2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.matrix.android.sdk.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * This worker creates a new work for each events passed in parameter + * + * Possible previous worker: Always [UploadContentWorker] + * Possible next worker : None, but it will post new work to send events, encrypted or not + */ +internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List<Event>, + val isEncrypted: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + @Inject lateinit var localEchoRepository: LocalEchoRepository + + override suspend fun doWork(): Result { + Timber.v("Start dispatch sending multiple event work") + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + if (params.lastFailureMessage != null) { + params.events.forEach { event -> + event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) } + } + // Transmit the error if needed? + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + // Create a work for every event + params.events.forEach { event -> + if (params.isEncrypted) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(params.sessionId, event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(params.sessionId, event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(params.sessionId, event, true) + timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) + } + } + + return Result.success() + } + + private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b9e1ec9d84bec8306d64a8193ced4330fb63c63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.Data +import androidx.work.InputMerger + +/** + * InputMerger which takes only the first input, to ensure an appended work will only have the specified parameters + */ +internal class NoMerger : InputMerger() { + override fun merge(inputs: MutableList<Data>): Data { + return inputs.first() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..e1e780c35a66c23b06f35591fea2473491712da5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val txID: String, + val roomId: String, + val eventId: String, + val reason: String?, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val eventId = params.eventId + return runCatching { + executeRequest<SendResponse>(eventBus) { + apiCall = roomAPI.redactEvent( + params.txID, + params.roomId, + eventId, + if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) + } + }.fold( + { + Result.success() + }, + { + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + // TODO mark as failed to send? + // always return success, or the chain will be stuck for ever! + Result.success(WorkerParamsFactory.toData(params.copy( + lastFailureMessage = it.localizedMessage + ))) + } + } + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt new file mode 100644 index 0000000000000000000000000000000000000000..65c692f42e199cdc2c15a3a099a68393a5b67f73 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.BackoffPolicy +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class RoomEventSender @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon, + @SessionId private val sessionId: String, + private val cryptoService: CryptoService +) { + fun sendEvent(event: Event): Cancelable { + // Encrypted room handling + return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(event, true) + timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork) + } + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..2868ce29c1f86a1496667476a56686847fa9458f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 + +/** + * Possible previous worker: [EncryptEventWorker] or first worker + * Possible next worker : None + */ +internal class SendEventWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + // TODO remove after some time, it's used for compat + val event: Event? = null, + val eventId: String? = null, + val roomId: String? = null, + val type: String? = null, + val contentStr: String? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams { + + constructor(sessionId: String, event: Event, lastFailureMessage: String? = null) : this( + sessionId = sessionId, + eventId = event.eventId, + roomId = event.roomId, + type = event.type, + contentStr = ContentMapper.map(event.content), + lastFailureMessage = lastFailureMessage + ) + } + + @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + if (params.eventId == null || params.roomId == null || params.type == null) { + // compat with old params, make it fail if any + if (params.event?.eventId != null) { + localEchoRepository.updateSendState(params.event.eventId, SendState.UNDELIVERED) + } + return Result.success() + } + if (params.lastFailureMessage != null) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + return try { + sendEvent(params.eventId, params.roomId, params.type, params.contentStr) + Result.success() + } catch (exception: Throwable) { + // It does start from 0, we want it to stop if it fails the third time + val currentAttemptCount = runAttemptCount + 1 + if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + return Result.success() + } else { + Result.retry() + } + } + } + + private suspend fun sendEvent(eventId: String, roomId: String, type: String, contentStr: String?) { + localEchoRepository.updateSendState(eventId, SendState.SENDING) + executeRequest<SendResponse>(eventBus) { + apiCall = roomAPI.send(eventId, roomId, type, contentStr) + } + localEchoRepository.updateSendState(eventId, SendState.SENT) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9ba553ce5356e99dc91a3701d894c15c9a13d98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SendResponse( + /** + * A unique identifier for the event. + */ + @Json(name = "event_id") val eventId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..33490a4a03cdfbc318a2bff83ca899ffefb5f776 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply + +/** + * Contains a text and eventually a formatted text + */ +data class TextContent( + val text: String, + val formattedText: String? = null +) { + fun takeFormatted() = formattedText ?: text +} + +fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { + return MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, + body = text, + formattedBody = formattedText + ) +} + +fun TextContent.removeInReplyFallbacks(): TextContent { + return copy( + text = extractUsefulTextFromReply(this.text), + formattedText = this.formattedText?.let { extractUsefulTextFromHtmlReply(it) } + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt new file mode 100644 index 0000000000000000000000000000000000000000..18063bbc1e16609030e13bfd55a98223bc89a953 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send.pills + +import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan + +internal data class MentionLinkSpec( + val span: MatrixItemSpan, + val start: Int, + val end: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6bc19f3ae349413ba4f9538541ab6217ca20c78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send.pills + +import javax.inject.Inject + +internal class MentionLinkSpecComparator @Inject constructor() : Comparator<MentionLinkSpec> { + + override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int { + return when { + o1.start < o2.start -> -1 + o1.start > o2.start -> 1 + o1.end < o2.end -> 1 + o1.end > o2.end -> -1 + else -> 0 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..e0fe580cba8a0f69cdc1083e79f70ed2843bd37a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.send.pills + +import android.text.SpannableString +import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan +import java.util.Collections +import javax.inject.Inject + +/** + * Utility class to detect special span in CharSequence and turn them into + * formatted text to send them as a Matrix messages. + */ +internal class TextPillsUtils @Inject constructor( + private val mentionLinkSpecComparator: MentionLinkSpecComparator +) { + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToHtml(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE) + } + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToMarkdown(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE) + } + + private fun transformPills(text: CharSequence, template: String): String? { + val spannableString = SpannableString.valueOf(text) + val pills = spannableString + ?.getSpans(0, text.length, MatrixItemSpan::class.java) + ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } + ?.toMutableList() + ?.takeIf { it.isNotEmpty() } + ?: return null + + // we need to prune overlaps! + pruneOverlaps(pills) + + return buildString { + var currIndex = 0 + pills.forEachIndexed { _, (urlSpan, start, end) -> + // We want to replace with the pill with a html link + // append text before pill + append(text, currIndex, start) + // append the pill + append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName())) + currIndex = end + } + // append text after the last pill + append(text, currIndex, text.length) + } + } + + private fun pruneOverlaps(links: MutableList<MentionLinkSpec>) { + Collections.sort(links, mentionLinkSpecComparator) + var len = links.size + var i = 0 + while (i < len - 1) { + val a = links[i] + val b = links[i + 1] + var remove = -1 + + // test if there is an overlap + if (b.start in a.start until a.end) { + when { + b.end <= a.end -> + // b is inside a -> b should be removed + remove = i + 1 + a.end - a.start > b.end - b.start -> + // overlap and a is bigger -> b should be removed + remove = i + 1 + a.end - a.start < b.end - b.start -> + // overlap and a is smaller -> a should be removed + remove = i + } + + if (remove != -1) { + links.removeAt(remove) + len-- + continue + } + } + i++ + } + } + + companion object { + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" + + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0150acd1e2682e65187e8a7ceac18a8e236bf61f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.content.FileUploader +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, + private val stateEventDataSource: StateEventDataSource, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val fileUploader: FileUploader, + private val addRoomAliasTask: AddRoomAliasTask +) : StateService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): StateService + } + + override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { + return stateEventDataSource.getStateEvent(roomId, eventType, stateKey) + } + + override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> { + return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey) + } + + override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> { + return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey) + } + + override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> { + return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) + } + + override fun sendStateEvent( + eventType: String, + stateKey: String?, + body: JsonDict, + callback: MatrixCallback<Unit> + ): Cancelable { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = stateKey, + eventType = eventType, + body = body + ) + return sendStateTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_TOPIC, + body = mapOf("topic" to topic), + callback = callback, + stateKey = null + ) + } + + override fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_NAME, + body = mapOf("name" to name), + callback = callback, + stateKey = null + ) + } + + override fun addRoomAlias(roomAlias: String, callback: MatrixCallback<Unit>): Cancelable { + return addRoomAliasTask + .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateCanonicalAlias(alias: String, callback: MatrixCallback<Unit>): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, + body = mapOf("alias" to alias), + callback = callback, + stateKey = null + ) + } + + override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + body = mapOf("history_visibility" to readability), + callback = callback, + stateKey = null + ) + } + + override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + callback = callback, + stateKey = null + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..52e865c4e29934746a3d8c4652579f53a447f2a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendStateTask : Task<SendStateTask.Params, Unit> { + data class Params( + val roomId: String, + val stateKey: String?, + val eventType: String, + val body: JsonDict + ) +} + +internal class DefaultSendStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : SendStateTask { + + override suspend fun execute(params: SendStateTask.Params) { + return executeRequest(eventBus) { + apiCall = if (params.stateKey == null) { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + params = params.body + ) + } else { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + stateKey = params.stateKey, + params = params.body + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..e8dc2ddf4027486cde64cd2eb5ee05dc5b9dfa0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where +import javax.inject.Inject + +internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() + } + } + + fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, eventTypes, stateKey) + .findAll() + .mapNotNull { + it.root?.asDomain() + } + } + } + + fun getStateEventsLive(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.filterNotNull() + } + } + + private fun buildStateEventQuery(realm: Realm, + roomId: String, + eventTypes: Set<String>, + stateKey: QueryStringValue + ): RealmQuery<CurrentStateEventEntity> { + return realm.where<CurrentStateEventEntity>() + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..a43241a657c59ccdda66ce9193087083b0cd03fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.findByAlias +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomSummaryMapper: RoomSummaryMapper) { + + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return monarchy + .fetchCopyMap({ + if (roomIdOrAlias.startsWith("!")) { + // It's a roomId + RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst() + } else { + // Assume it's a room alias + RoomSummaryEntity.findByAlias(it, roomIdOrAlias) + } + }, { entity, _ -> + roomSummaryMapper.map(entity) + }) + } + + fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, + { roomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary> { + return monarchy.fetchAllMappedSync( + { roomSummariesQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> { + return monarchy.findAllMappedWithChanges( + { roomSummariesQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> { + return monarchy.fetchAllMappedSync( + { breadcrumbsQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> { + return monarchy.findAllMappedWithChanges( + { breadcrumbsQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> { + return roomSummariesQuery(realm, queryParams) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + } + + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> { + val query = RoomSummaryEntity.where(realm) + query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) + query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + return query + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt new file mode 100644 index 0000000000000000000000000000000000000000..99671c232a1ca4092ed89594647fbe4b19a38b86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.summary + +import dagger.Lazy +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +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.RoomAliasesContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver +import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class RoomSummaryUpdater @Inject constructor( + @UserId private val userId: String, + private val roomDisplayNameResolver: RoomDisplayNameResolver, + private val roomAvatarResolver: RoomAvatarResolver, + private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>, + private val eventBus: EventBus) { + + companion object { + // TODO: maybe allow user of SDK to give that list + val PREVIEWABLE_TYPES = listOf( + // TODO filter message type (KEY_VERIFICATION_READY, etc.) + EventType.MESSAGE, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER, + EventType.ENCRYPTED, + EventType.STATE_ROOM_ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.REACTION, + EventType.STATE_ROOM_CREATE + ) + } + + fun update(realm: Realm, + roomId: String, + membership: Membership? = null, + roomSummary: RoomSyncSummary? = null, + unreadNotifications: RoomSyncUnreadNotifications? = null, + updateMembers: Boolean = false, + inviterId: String? = null) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + if (roomSummary != null) { + if (roomSummary.heroes.isNotEmpty()) { + roomSummaryEntity.heroes.clear() + roomSummaryEntity.heroes.addAll(roomSummary.heroes) + } + if (roomSummary.invitedMembersCount != null) { + roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount + } + if (roomSummary.joinedMembersCount != null) { + roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount + } + } + roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0 + roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0 + + if (membership != null) { + roomSummaryEntity.membership = membership + } + + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, + filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) + + val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root + val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root + val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root + val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + + // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room + val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .findFirst() + + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 + // avoid this call if we are sure there are unread events + || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) + + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString() + roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) + roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name + roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent + roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>() + ?.canonicalAlias + + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases + .orEmpty() + roomSummaryEntity.aliases.clear() + roomSummaryEntity.aliases.addAll(roomAliases) + roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.isEncrypted = encryptionEvent != null + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs + + if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { + roomSummaryEntity.inviterId = inviterId + } else if (roomSummaryEntity.membership != Membership.INVITE) { + roomSummaryEntity.inviterId = null + } + roomSummaryEntity.updateHasFailedSending() + + if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { + Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") + timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) + } + + if (updateMembers) { + val otherRoomMembers = RoomMemberHelper(realm, roomId) + .queryActiveRoomMembersEvent() + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .findAll() + .asSequence() + .map { it.userId } + + roomSummaryEntity.otherMemberIds.clear() + roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) + if (roomSummaryEntity.isEncrypted) { + eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId)) + } + } + } + + private fun RoomSummaryEntity.updateHasFailedSending() { + hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty() + } + + fun updateSendingInformation(realm: Realm, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + roomSummaryEntity.updateHasFailedSending() + roomSummaryEntity.latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, + filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) + } + + fun updateShieldTrust(realm: Realm, + roomId: String, + trust: RoomEncryptionTrustLevel?) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + if (roomSummaryEntity.isEncrypted) { + roomSummaryEntity.roomEncryptionTrustLevel = trust + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d78a7f338a77ba5622ec9fb994e980e5485f1615 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddTagToRoomTask : Task<AddTagToRoomTask.Params, Unit> { + + data class Params( + val roomId: String, + val tag: String, + val order: Double? + ) +} + +internal class DefaultAddTagToRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : AddTagToRoomTask { + + override suspend fun execute(params: AddTagToRoomTask.Params) { + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.putTag( + userId = userId, + roomId = params.roomId, + tag = params.tag, + body = TagBody( + order = params.order + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..141adad6438660933c2bcffe75483ddc26708aa4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultTagsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val addTagToRoomTask: AddTagToRoomTask, + private val deleteTagFromRoomTask: DeleteTagFromRoomTask +) : TagsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TagsService + } + + override fun addTag(tag: String, order: Double?, callback: MatrixCallback<Unit>): Cancelable { + val params = AddTagToRoomTask.Params(roomId, tag, order) + return addTagToRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable { + val params = DeleteTagFromRoomTask.Params(roomId, tag) + return deleteTagFromRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..901733850302d5fc59e135f1f49e4e0164edf776 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteTagFromRoomTask : Task<DeleteTagFromRoomTask.Params, Unit> { + + data class Params( + val roomId: String, + val tag: String + ) +} + +internal class DefaultDeleteTagFromRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : DeleteTagFromRoomTask { + + override suspend fun execute(params: DeleteTagFromRoomTask.Params) { + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.deleteTag( + userId = userId, + roomId = params.roomId, + tag = params.tag + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..33d39ba4d194e0cbcc065e476aaff154d85d9a8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TagBody( + /** + * A number in a range [0,1] describing a relative position of the room under the given tag. + */ + @Json(name = "order") + val order: Double? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d880c04280d623b53f4341b3e121eda1ec0a151 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEventPersistor.Result> { + + data class Params( + val roomId: String, + val eventId: String + ) +} + +internal class DefaultGetContextOfEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val filterRepository: FilterRepository, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, + private val eventBus: EventBus +) : GetContextOfEventTask { + + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val response = executeRequest<EventContextResponse>(eventBus) { + // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) + } + return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.FORWARDS) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..55b8fb6ff4d6f4b3b039722507844cd78e343b37 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> { + + data class Params( + val roomId: String, + val from: String, + val direction: PaginationDirection, + val limit: Int + ) +} + +internal class DefaultPaginationTask @Inject constructor( + private val roomAPI: RoomAPI, + private val filterRepository: FilterRepository, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, + private val eventBus: EventBus +) : PaginationTask { + + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val chunk = executeRequest<PaginationResponse>(eventBus) { + isRetryable = true + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) + } + return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt new file mode 100644 index 0000000000000000000000000000000000000000..b4c32c045e0174ebdc63b6e127ff6e4863245804 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -0,0 +1,798 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.MatrixCallback +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.RelationType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.util.CancelableBag +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import org.matrix.android.sdk.internal.util.createUIHandler +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import timber.log.Timber +import java.util.Collections +import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlin.math.max + +private const val MIN_FETCHING_COUNT = 30 + +internal class DefaultTimeline( + private val roomId: String, + private var initialEventId: String? = null, + private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor, + private val contextOfEventTask: GetContextOfEventTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val paginationTask: PaginationTask, + private val timelineEventMapper: TimelineEventMapper, + private val settings: TimelineSettings, + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val eventBus: EventBus, + private val eventDecryptor: TimelineEventDecryptor +) : Timeline, TimelineHiddenReadReceipts.Delegate { + + data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>) + data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent) + + companion object { + val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") + } + + private val listeners = CopyOnWriteArrayList<Timeline.Listener>() + private val isStarted = AtomicBoolean(false) + private val isReady = AtomicBoolean(false) + private val mainHandler = createUIHandler() + private val backgroundRealm = AtomicReference<Realm>() + private val cancelableBag = CancelableBag() + private val debouncer = Debouncer(mainHandler) + + private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity> + private lateinit var filteredEvents: RealmResults<TimelineEventEntity> + private lateinit var sendingEvents: RealmResults<TimelineEventEntity> + + private var prevDisplayIndex: Int? = null + private var nextDisplayIndex: Int? = null + private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) + private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) + private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) + private val backwardsState = AtomicReference(State()) + private val forwardsState = AtomicReference(State()) + + override val timelineID = UUID.randomUUID().toString() + + override val isLive + get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) + + private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet -> + if (!results.isLoaded || !results.isValid) { + return@OrderedRealmCollectionChangeListener + } + handleUpdates(results, changeSet) + } + + // Public methods ****************************************************************************** + + override fun paginate(direction: Timeline.Direction, count: Int) { + BACKGROUND_HANDLER.post { + if (!canPaginate(direction)) { + return@post + } + Timber.v("Paginate $direction of $count items") + val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) + if (shouldPostSnapshot) { + postSnapshot() + } + } + } + + override fun pendingEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 + } + } + + override fun failedToDeliverEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() + } + } + + override fun start() { + if (isStarted.compareAndSet(false, true)) { + Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + eventBus.register(this) + BACKGROUND_HANDLER.post { + eventDecryptor.start() + val realm = Realm.getInstance(realmConfiguration) + backgroundRealm.set(realm) + + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: throw IllegalStateException("Can't open a timeline without a room") + + sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() + sendingEvents.addChangeListener { events -> + // Remove in memory as soon as they are known by database + events.forEach { te -> + inMemorySendingEvents.removeAll { te.eventId == it.eventId } + } + postSnapshot() + } + nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + filteredEvents = nonFilteredEvents.where() + .filterEventsWithSettings() + .findAll() + nonFilteredEvents.addChangeListener(eventsChangeListener) + handleInitialLoad() + if (settings.shouldHandleHiddenReadReceipts()) { + hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) + } + isReady.set(true) + } + } + } + + private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { + return buildReadReceipts && (filterEdits || filterTypes) + } + + override fun dispose() { + if (isStarted.compareAndSet(true, false)) { + isReady.set(false) + eventBus.unregister(this) + Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + cancelableBag.cancel() + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) + BACKGROUND_HANDLER.post { + if (this::sendingEvents.isInitialized) { + sendingEvents.removeAllChangeListeners() + } + if (this::nonFilteredEvents.isInitialized) { + nonFilteredEvents.removeAllChangeListeners() + } + if (settings.shouldHandleHiddenReadReceipts()) { + hiddenReadReceipts.dispose() + } + clearAllValues() + backgroundRealm.getAndSet(null).also { + it?.close() + } + eventDecryptor.destroy() + } + } + } + + override fun restartWithEventId(eventId: String?) { + dispose() + initialEventId = eventId + start() + postSnapshot() + } + + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { + return builtEvents.getOrNull(index) + } + + override fun getIndexOfEvent(eventId: String?): Int? { + return builtEventsIdMap[eventId] + } + + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { + return builtEventsIdMap[eventId]?.let { + getTimelineEventAtIndex(it) + } + } + + override fun getFirstDisplayableEventId(eventId: String): String? { + // If the item is built, the id is obviously displayable + val builtIndex = builtEventsIdMap[eventId] + if (builtIndex != null) { + return eventId + } + // Otherwise, we should check if the event is in the db, but is hidden because of filters + return Realm.getInstance(realmConfiguration).use { localRealm -> + val nonFilteredEvents = buildEventQuery(localRealm) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() + + val nonFilteredEvent = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + + val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll() + val isEventInDb = nonFilteredEvent != null + + val isHidden = isEventInDb && filteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() == null + + if (isHidden) { + val displayIndex = nonFilteredEvent?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = filteredEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) + .findFirst() + firstDisplayedEvent?.eventId + } else { + null + } + } else { + null + } + } + } + + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return hasMoreInCache(direction) || !hasReachedEnd(direction) + } + + override fun addListener(listener: Timeline.Listener): Boolean { + if (listeners.contains(listener)) { + return false + } + return listeners.add(listener).also { + postSnapshot() + } + } + + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) + } + + override fun removeAllListeners() { + listeners.clear() + } + +// TimelineHiddenReadReceipts.Delegate + + override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean { + return rebuildEvent(eventId) { te -> + te.copy(readReceipts = readReceipts) + } + } + + override fun onReadReceiptsUpdated() { + postSnapshot() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { + if (isLive && onNewTimelineEvents.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(onNewTimelineEvents.eventIds) + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { + if (isLive && onLocalEchoCreated.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + Timber.v("On local echo created: $onLocalEchoCreated") + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot() + } + } + +// Private methods ***************************************************************************** + + private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + // Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = builder(te) + true + } + } ?: false + } + + private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache + + private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd + + private fun updateLoadingStates(results: RealmResults<TimelineEventEntity>) { + val lastCacheEvent = results.lastOrNull() + val firstCacheEvent = results.firstOrNull() + val chunkEntity = getLiveChunk() + + updateState(Timeline.Direction.FORWARDS) { + it.copy( + hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), + hasReachedEnd = chunkEntity?.isLastForward ?: false + ) + } + updateState(Timeline.Direction.BACKWARDS) { + it.copy( + hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), + hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE + ) + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * @return true if createSnapshot should be posted + */ + private fun paginateInternal(startDisplayIndex: Int?, + direction: Timeline.Direction, + count: Int): Boolean { + if (count == 0) { + return false + } + updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) + val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) + if (shouldFetchMore) { + val newRequestedCount = count - builtCount + updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) } + val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) + executePaginationTask(direction, fetchingCount) + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + return !shouldFetchMore + } + + private fun createSnapshot(): List<TimelineEvent> { + return buildSendingEvents() + builtEvents.toList() + } + + private fun buildSendingEvents(): List<TimelineEvent> { + val builtSendingEvents = ArrayList<TimelineEvent>() + if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { + builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) + sendingEvents.forEach { timelineEventEntity -> + if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { + builtSendingEvents.add(timelineEventMapper.map(timelineEventEntity)) + } + } + } + return builtSendingEvents + } + + private fun canPaginate(direction: Timeline.Direction): Boolean { + return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) + } + + private fun getState(direction: Timeline.Direction): State { + return when (direction) { + Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.BACKWARDS -> backwardsState.get() + } + } + + private fun updateState(direction: Timeline.Direction, update: (State) -> State) { + val stateReference = when (direction) { + Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.BACKWARDS -> backwardsState + } + val currentValue = stateReference.get() + val newValue = update(currentValue) + stateReference.set(newValue) + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun handleInitialLoad() { + var shouldFetchInitialEvent = false + val currentInitialEventId = initialEventId + val initialDisplayIndex = if (currentInitialEventId == null) { + nonFilteredEvents.firstOrNull()?.displayIndex + } else { + val initialEvent = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) + .findFirst() + + shouldFetchInitialEvent = initialEvent == null + initialEvent?.displayIndex + } + prevDisplayIndex = initialDisplayIndex + nextDisplayIndex = initialDisplayIndex + if (currentInitialEventId != null && shouldFetchInitialEvent) { + fetchEvent(currentInitialEventId) + } else { + val count = filteredEvents.size.coerceAtMost(settings.initialSize) + if (initialEventId == null) { + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + } else { + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1)) + paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1)) + } + } + postSnapshot() + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) { + // If changeSet has deletion we are having a gap, so we clear everything + if (changeSet.deletionRanges.isNotEmpty()) { + clearAllValues() + } + var postSnapshot = false + changeSet.insertionRanges.forEach { range -> + val (startDisplayIndex, direction) = if (range.startIndex == 0) { + Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + } + val state = getState(direction) + if (state.isPaginating) { + // We are getting new items from pagination + postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount) + } else { + // We are getting new items from sync + buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) + postSnapshot = true + } + } + changeSet.changes.forEach { index -> + val eventEntity = results[index] + eventEntity?.eventId?.let { eventId -> + postSnapshot = rebuildEvent(eventId) { + buildTimelineEvent(eventEntity) + } || postSnapshot + } + } + if (postSnapshot) { + postSnapshot() + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { + val currentChunk = getLiveChunk() + val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken + if (token == null) { + if (direction == Timeline.Direction.BACKWARDS + || (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { + // We are in the case where event exists, but we do not know the token. + // Fetch (again) the last event to get a token + val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { + nonFilteredEvents.firstOrNull()?.eventId + } else { + nonFilteredEvents.lastOrNull()?.eventId + } + if (lastKnownEventId == null) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } else { + val params = FetchTokenAndPaginateTask.Params( + roomId = roomId, + limit = limit, + direction = direction.toPaginationDirection(), + lastKnownEventId = lastKnownEventId + ) + cancelableBag += fetchTokenAndPaginateTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + } else { + val params = PaginationTask.Params( + roomId = roomId, + from = token, + direction = direction.toPaginationDirection(), + limit = limit + ) + Timber.v("Should fetch $limit items $direction") + cancelableBag += paginationTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } + } + + // For debug purpose only + private fun dumpAndLogChunks() { + val liveChunk = getLiveChunk() + Timber.w("Live chunk: $liveChunk") + + Realm.getInstance(realmConfiguration).use { realm -> + ChunkEntity.where(realm, roomId).findAll() + .also { Timber.w("Found ${it.size} chunks") } + .forEach { + Timber.w("") + Timber.w("ChunkEntity: $it") + Timber.w("prevToken: ${it.prevToken}") + Timber.w("nextToken: ${it.nextToken}") + Timber.w("isLastBackward: ${it.isLastBackward}") + Timber.w("isLastForward: ${it.isLastForward}") + it.timelineEvents.forEach { tle -> + Timber.w(" TLE: ${tle.root?.content}") + } + } + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun getTokenLive(direction: Timeline.Direction): String? { + val chunkEntity = getLiveChunk() ?: return null + return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * Return the current Chunk + */ + private fun getLiveChunk(): ChunkEntity? { + return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * @return the number of items who have been added + */ + private fun buildTimelineEvents(startDisplayIndex: Int?, + direction: Timeline.Direction, + count: Long): Int { + if (count < 1 || startDisplayIndex == null) { + return 0 + } + val start = System.currentTimeMillis() + val offsetResults = getOffsetResults(startDisplayIndex, direction, count) + if (offsetResults.isEmpty()) { + return 0 + } + val offsetIndex = offsetResults.last()!!.displayIndex + if (direction == Timeline.Direction.BACKWARDS) { + prevDisplayIndex = offsetIndex - 1 + } else { + nextDisplayIndex = offsetIndex + 1 + } + offsetResults.forEach { eventEntity -> + + val timelineEvent = buildTimelineEvent(eventEntity) + val transactionId = timelineEvent.root.unsignedData?.transactionId + val sendingEvent = inMemorySendingEvents.find { + it.eventId == transactionId + } + inMemorySendingEvents.remove(sendingEvent) + + if (timelineEvent.isEncrypted() + && timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) } + } + + val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size + builtEvents.add(position, timelineEvent) + // Need to shift :/ + builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } + builtEventsIdMap[eventEntity.eventId] = position + } + val time = System.currentTimeMillis() - start + Timber.v("Built ${offsetResults.size} items from db in $time ms") + // For the case where wo reach the lastForward chunk + updateLoadingStates(filteredEvents) + return offsetResults.size + } + + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts, + correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + ) + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun getOffsetResults(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): RealmResults<TimelineEventEntity> { + val offsetQuery = filteredEvents.where() + if (direction == Timeline.Direction.BACKWARDS) { + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } + return offsetQuery + .limit(count) + .findAll() + } + + private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> { + return if (initialEventId == null) { + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true) + } else { + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId)) + } + } + + private fun fetchEvent(eventId: String) { + val params = GetContextOfEventTask.Params(roomId, eventId) + cancelableBag += contextOfEventTask.configureWith(params) { + callback = object : MatrixCallback<TokenChunkEventPersistor.Result> { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + postSnapshot() + } + + override fun onFailure(failure: Throwable) { + postFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + private fun postSnapshot() { + BACKGROUND_HANDLER.post { + if (isReady.get().not()) { + return@post + } + updateLoadingStates(filteredEvents) + val snapshot = createSnapshot() + val runnable = Runnable { + listeners.forEach { + it.onTimelineUpdated(snapshot) + } + } + debouncer.debounce("post_snapshot", runnable, 1) + } + } + + private fun postFailure(throwable: Throwable) { + if (isReady.get().not()) { + return + } + val runnable = Runnable { + listeners.forEach { + it.onTimelineFailure(throwable) + } + } + mainHandler.post(runnable) + } + + private fun clearAllValues() { + prevDisplayIndex = null + nextDisplayIndex = null + builtEvents.clear() + builtEventsIdMap.clear() + backwardsState.set(State()) + forwardsState.set(State()) + } + + private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback<TokenChunkEventPersistor.Result> { + return object : MatrixCallback<TokenChunkEventPersistor.Result> { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + when (data) { + TokenChunkEventPersistor.Result.SUCCESS -> { + Timber.v("Success fetching $limit items $direction from pagination request") + } + TokenChunkEventPersistor.Result.REACHED_END -> { + postSnapshot() + } + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } + } + } + + override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() + Timber.v("Failure fetching $limit items $direction from pagination request") + } + } + } + + // Extension methods *************************************************************************** + + private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { + return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS + } + + private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> { + if (settings.filterTypes) { + `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) + } + if (settings.filterUseless) { + not() + .equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true) + } + if (settings.filterEdits) { + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) + } + return this + } + + private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> { + return filter { + val filterType = !settings.filterTypes || settings.allowedTypes.contains(it.root.type) + if (!filterType) return@filter false + + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { + val messageContent = it.root.content.toModel<MessageContent>() + messageContent?.relatesTo?.type != RelationType.REPLACE + } else { + true + } + if (!filterEdits) return@filter false + + val filterRedacted = !settings.filterRedacted || it.root.isRedacted() + + filterRedacted + } + } + + private data class State( + val hasReachedEnd: Boolean = false, + val hasMoreInCache: Boolean = true, + val isPaginating: Boolean = false, + val requestedPaginationCount: Int = 0 + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt new file mode 100644 index 0000000000000000000000000000000000000000..db675f69f5bd64253eeccece8ee17f82f749cf46 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Sort +import io.realm.kotlin.where +import org.greenrobot.eventbus.EventBus + +internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus, + private val taskExecutor: TaskExecutor, + private val contextOfEventTask: GetContextOfEventTask, + private val eventDecryptor: TimelineEventDecryptor, + private val paginationTask: PaginationTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper +) : TimelineService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TimelineService + } + + override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { + return DefaultTimeline( + roomId = roomId, + initialEventId = eventId, + realmConfiguration = monarchy.realmConfiguration, + taskExecutor = taskExecutor, + contextOfEventTask = contextOfEventTask, + paginationTask = paginationTask, + timelineEventMapper = timelineEventMapper, + settings = settings, + hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + eventBus = eventBus, + eventDecryptor = eventDecryptor, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask + ) + } + + override fun getTimeLineEvent(eventId: String): TimelineEvent? { + return monarchy + .fetchCopyMap({ + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() + }, { entity, _ -> + timelineEventMapper.map(entity) + }) + } + + override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { + val liveData = monarchy.findAllMappedWithChanges( + { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) }, + { timelineEventMapper.map(it) } + ) + return Transformations.map(liveData) { events -> + events.firstOrNull().toOptional() + } + } + + override fun getAttachmentMessages(): List<TimelineEvent> { + // TODO pretty bad query.. maybe we should denormalize clear type in base? + return doWithRealm(monarchy.realmConfiguration) { realm -> + realm.where<TimelineEventEntity>() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .findAll() + ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } } + ?: emptyList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..27006c8183a1da7718f8e1a42b3a2b97edc235e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +data class EventContextResponse( + @Json(name = "event") val event: Event, + @Json(name = "start") override val start: String? = null, + @Json(name = "events_before") val eventsBefore: List<Event> = emptyList(), + @Json(name = "events_after") val eventsAfter: List<Event> = emptyList(), + @Json(name = "end") override val end: String? = null, + @Json(name = "state") override val stateEvents: List<Event> = emptyList() +) : TokenChunkEvent { + + override val events: List<Event> by lazy { + eventsAfter.reversed() + listOf(event) + eventsBefore + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..23a32996fa472aaecc2999de8a550abbf97ca30c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.query.findIncludingEvent +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchTokenAndPaginateTask : Task<FetchTokenAndPaginateTask.Params, TokenChunkEventPersistor.Result> { + + data class Params( + val roomId: String, + val lastKnownEventId: String, + val direction: PaginationDirection, + val limit: Int + ) +} + +internal class DefaultFetchTokenAndPaginateTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val filterRepository: FilterRepository, + private val paginationTask: PaginationTask, + private val eventBus: EventBus +) : FetchTokenAndPaginateTask { + + override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val response = executeRequest<EventContextResponse>(eventBus) { + apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) + } + val fromToken = if (params.direction == PaginationDirection.FORWARDS) { + response.end + } else { + response.start + } + ?: throw IllegalStateException("No token found") + + monarchy.awaitTransaction { realm -> + val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.lastKnownEventId) + if (params.direction == PaginationDirection.FORWARDS) { + chunkToUpdate?.nextToken = fromToken + } else { + chunkToUpdate?.prevToken = fromToken + } + } + val paginationParams = PaginationTask.Params( + roomId = params.roomId, + from = fromToken, + direction = params.direction, + limit = params.limit + ) + return paginationTask.execute(paginationParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..531fac4a5761eb436972a7812ea77d368ff3a08b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +// TODO Add parent task + +internal class GetEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : Task<GetEventTask.Params, Event> { + + internal data class Params( + val roomId: String, + val eventId: String + ) + + override suspend fun execute(params: Params): Event { + return executeRequest(eventBus) { + apiCall = roomAPI.getEvent(params.roomId, params.eventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt new file mode 100644 index 0000000000000000000000000000000000000000..581f23c4033cda170f87e5f64ac3d56f14bd6df4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +internal enum class PaginationDirection(val value: String) { + /** + * Forwards when the event is added to the end of the timeline. + * These events come from the /sync stream or from forwards pagination. + */ + FORWARDS("f"), + + /** + * Backwards when the event is added to the start of the timeline. + * These events come from a back pagination. + */ + BACKWARDS("b"); + + fun reversed(): PaginationDirection { + return when (this) { + FORWARDS -> BACKWARDS + BACKWARDS -> FORWARDS + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0f2e693dc67df78ac157737d3ce33e20d700c77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class PaginationResponse( + @Json(name = "start") override val start: String? = null, + @Json(name = "end") override val end: String? = null, + @Json(name = "chunk") override val events: List<Event> = emptyList(), + @Json(name = "state") override val stateEvents: List<Event> = emptyList() +) : TokenChunkEvent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ca0d19b5eb8dfee56de284e5a69a4b33cfff7c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import javax.inject.Inject + +@SessionScope +internal class TimelineEventDecryptor @Inject constructor( + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val cryptoService: CryptoService +) { + + private val newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + synchronized(unknownSessionsFailure) { + unknownSessionsFailure[sessionId] + ?.toList() + .orEmpty() + .also { + unknownSessionsFailure[sessionId]?.clear() + } + }.forEach { + requestDecryption(it) + } + } + } + + private var executor: ExecutorService? = null + + // Set of eventIds which are currently decrypting + private val existingRequests = mutableSetOf<DecryptionRequest>() + // sessionId -> list of eventIds + private val unknownSessionsFailure = mutableMapOf<String, MutableSet<DecryptionRequest>>() + + fun start() { + executor = Executors.newSingleThreadExecutor() + cryptoService.addNewSessionListener(newSessionListener) + } + + fun destroy() { + cryptoService.removeSessionListener(newSessionListener) + executor?.shutdownNow() + executor = null + synchronized(unknownSessionsFailure) { + unknownSessionsFailure.clear() + } + synchronized(existingRequests) { + existingRequests.clear() + } + } + + fun requestDecryption(request: DecryptionRequest) { + synchronized(unknownSessionsFailure) { + for (requests in unknownSessionsFailure.values) { + if (request in requests) { + Timber.d("Skip Decryption request for event ${request.eventId}, unknown session") + return + } + } + } + synchronized(existingRequests) { + if (!existingRequests.add(request)) { + Timber.d("Skip Decryption request for event ${request.eventId}, already requested") + return + } + } + executor?.execute { + Realm.getInstance(realmConfiguration).use { realm -> + processDecryptRequest(request, realm) + } + } + } + + private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction { + val eventId = request.eventId + val timelineId = request.timelineId + Timber.v("Decryption request for event $eventId") + val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() + ?: return@executeTransaction Unit.also { + Timber.d("Decryption request for unknown message") + } + val event = eventEntity.asDomain() + try { + val result = cryptoService.decryptEvent(event, timelineId) + Timber.v("Successfully decrypted event $eventId") + eventEntity.setDecryptionResult(result) + } catch (e: MXCryptoError) { + Timber.v(e, "Failed to decrypt event $eventId") + if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) { + // Keep track of unknown sessions to automatically try to decrypt on new session + eventEntity.decryptionErrorCode = e.errorType.name + eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + event.content?.toModel<EncryptedEventContent>()?.let { content -> + content.sessionId?.let { sessionId -> + synchronized(unknownSessionsFailure) { + val list = unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } + list.add(request) + } + } + } + } + } catch (t: Throwable) { + Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") + } finally { + synchronized(existingRequests) { + existingRequests.remove(request) + } + } + } + + data class DecryptionRequest( + val eventId: String, + val timelineId: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt new file mode 100644 index 0000000000000000000000000000000000000000..426daa4b57d15efdd0463984d476284714608035 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import android.util.SparseArray +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.whereInRoom +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults + +/** + * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). + * When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event. + * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. + */ +internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val roomId: String, + private val settings: TimelineSettings) { + + interface Delegate { + fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean + fun onReadReceiptsUpdated() + } + + private val correctedReadReceiptsEventByIndex = SparseArray<String>() + private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>() + + private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity> + private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity> + private lateinit var filteredEvents: RealmResults<TimelineEventEntity> + private lateinit var delegate: Delegate + + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet -> + if (!collection.isLoaded || !collection.isValid) { + return@OrderedRealmCollectionChangeListener + } + var hasChange = false + // Deletion here means we don't have any readReceipts for the given hidden events + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex.get(it, "") + val timelineEvent = filteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + + // We are rebuilding the corresponding event with only his own RR + val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) + hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + for (index in 0 until hiddenReadReceipts.size) { + val summary = hiddenReadReceipts[index] ?: continue + val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue + val isLoaded = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null + val displayIndex = timelineEvent.displayIndex + + if (isLoaded) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = filteredEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent + .getOrPut(firstDisplayedEvent.eventId, { + ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) + }) + .addAll(readReceiptsSummaryMapper.map(summary)) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + val sortedReadReceipts = correctedReadReceipts.sortedByDescending { + it.originServerTs + } + hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange + } + } + if (hasChange) { + delegate.onReadReceiptsUpdated() + } + } + + /** + * Start the realm query subscription. Has to be called on an HandlerThread + */ + fun start(realm: Realm, + filteredEvents: RealmResults<TimelineEventEntity>, + nonFilteredEvents: RealmResults<TimelineEventEntity>, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents + this.delegate = delegate + // We are looking for read receipts set on hidden events. + // We only accept those with a timelineEvent (so coming from pagination/sync). + this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + } + + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ + fun dispose() { + if (this::hiddenReadReceipts.isInitialized) { + this.hiddenReadReceipts.removeAllChangeListeners() + } + } + + /** + * Return the current corrected [ReadReceipt] list for an event, or null + */ + fun correctedReadReceipts(eventId: String?): List<ReadReceipt>? { + return correctedReadReceiptsByEvent[eventId] + } + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. + */ + private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> { + beginGroup() + var needOr = false + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + needOr = true + } + if (settings.filterUseless) { + if (needOr) or() + equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true) + needOr = true + } + if (settings.filterEdits) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) + or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) + needOr = true + } + if (settings.filterRedacted) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) + } + endGroup() + return this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3124b68ca8a1303d69e4cdb63d343170949427a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.timeline + +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.startChain +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Helper class for sending event related works. + * All send event from a room are using the same workchain, in order to ensure order. + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send), + * if not the chain will be doomed in failed state. + */ +internal class TimelineSendEventWorkCommon @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { + + fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest): Cancelable { + return when { + workRequests.isEmpty() -> NoOpCancellable + workRequests.size == 1 -> postWork(roomId, workRequests.first()) + else -> { + val firstWork = workRequests.first() + var continuation = workManagerProvider.workManager + .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork) + for (i in 1 until workRequests.size) { + val workRequest = workRequests[i] + continuation = continuation.then(workRequest) + } + continuation.enqueue() + CancelableWork(workManagerProvider.workManager, firstWork.id) + } + } + } + + fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(roomId), policy, workRequest) + .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) + } + + inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun buildWorkName(roomId: String): String { + return "${roomId}_$SEND_WORK" + } + + fun cancelAllWorks(roomId: String) { + workManagerProvider.workManager + .cancelUniqueWork(buildWorkName(roomId)) + } + + companion object { + private const val SEND_WORK = "SEND_WORK" + private const val BACKOFF_DELAY = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..655af7c4e19d09a073df3e4745689b643f8a25d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event + +internal interface TokenChunkEvent { + val start: String? + val end: String? + val events: List<Event> + val stateEvents: List<Event> + + fun hasMore() = start != end +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt new file mode 100644 index 0000000000000000000000000000000000000000..da4eebe14222742bebfbb21005c36eaf664ea3c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +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.RoomMemberContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.helper.addOrUpdate +import org.matrix.android.sdk.internal.database.helper.addStateEvent +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.deleteOnCascade +import org.matrix.android.sdk.internal.database.helper.merge +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.create +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +/** + * Insert Chunk in DB, and eventually merge with existing chunk event + */ +internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + /** + * <pre> + * ======================================================================================================== + * | Backward case | + * ======================================================================================================== + * + * *--------------------------* *--------------------------* + * | startToken1 | | startToken1 | + * *--------------------------* *--------------------------* + * | | | | + * | | | | + * | receivedChunk backward | | | + * | Events | | | + * | | | | + * | | | | + * | | | | + * *--------------------------* *--------------------------* | | + * | startToken0 | | endToken1 | => | Merged chunk | + * *--------------------------* *--------------------------* | Events | + * | | | | + * | | | | + * | Current Chunk | | | + * | Events | | | + * | | | | + * | | | | + * | | | | + * *--------------------------* *--------------------------* + * | endToken0 | | endToken0 | + * *--------------------------* *--------------------------* + * + * + * ======================================================================================================== + * | Forward case | + * ======================================================================================================== + * + * *--------------------------* *--------------------------* + * | startToken0 | | startToken0 | + * *--------------------------* *--------------------------* + * | | | | + * | | | | + * | Current Chunk | | | + * | Events | | | + * | | | | + * | | | | + * | | | | + * *--------------------------* *--------------------------* | | + * | endToken0 | | startToken1 | => | Merged chunk | + * *--------------------------* *--------------------------* | Events | + * | | | | + * | | | | + * | receivedChunk forward | | | + * | Events | | | + * | | | | + * | | | | + * | | | | + * *--------------------------* *--------------------------* + * | endToken1 | | endToken1 | + * *--------------------------* *--------------------------* + * + * ======================================================================================================== + * </pre> + */ + + enum class Result { + SHOULD_FETCH_MORE, + REACHED_END, + SUCCESS + } + + suspend fun insertInDb(receivedChunk: TokenChunkEvent, + roomId: String, + direction: PaginationDirection): Result { + monarchy + .awaitTransaction { realm -> + Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") + + val nextToken: String? + val prevToken: String? + if (direction == PaginationDirection.FORWARDS) { + nextToken = receivedChunk.end + prevToken = receivedChunk.start + } else { + nextToken = receivedChunk.start + prevToken = receivedChunk.end + } + + val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) + val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) + + // The current chunk is the one we will keep all along the merge processChanges. + // We try to look for a chunk next to the token, + // otherwise we create a whole new one which is unlinked (not live) + val currentChunk = if (direction == PaginationDirection.FORWARDS) { + prevChunk?.apply { this.nextToken = nextToken } + } else { + nextChunk?.apply { this.prevToken = prevToken } + } + ?: ChunkEntity.create(realm, prevToken, nextToken) + + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + handleReachEnd(realm, roomId, direction, currentChunk) + } else { + handlePagination(realm, roomId, direction, receivedChunk, currentChunk) + } + } + return if (receivedChunk.events.isEmpty()) { + if (receivedChunk.start != receivedChunk.end) { + Result.SHOULD_FETCH_MORE + } else { + Result.REACHED_END + } + } else { + Result.SUCCESS + } + } + + private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { + Timber.v("Reach end of $roomId") + if (direction == PaginationDirection.FORWARDS) { + val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) + if (currentChunk != currentLastForwardChunk) { + currentChunk.isLastForward = true + currentLastForwardChunk?.deleteOnCascade() + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + latestPreviewableEvent = TimelineEventEntity.latestEvent( + realm, + roomId, + includesSending = true, + filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES + ) + } + } + } else { + currentChunk.isLastBackward = true + } + } + + private fun handlePagination( + realm: Realm, + roomId: String, + direction: PaginationDirection, + receivedChunk: TokenChunkEvent, + currentChunk: ChunkEntity + ) { + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() + val eventList = receivedChunk.events + val stateEvents = receivedChunk.stateEvents + + val now = System.currentTimeMillis() + + for (stateEvent in stateEvents) { + val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } + val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + currentChunk.addStateEvent(roomId, stateEventEntity, direction) + if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { + roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>() + } + } + val eventIds = ArrayList<String>(eventList.size) + for (event in eventList) { + if (event.eventId == null || event.senderId == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { now - it } + eventIds.add(event.eventId) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { + val contentToUse = if (direction == PaginationDirection.BACKWARDS) { + event.prevContent + } else { + event.content + } + roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>() + } + + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + } + // Find all the chunks which contain at least one event from the list of eventIds + val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) + Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") + val chunksToDelete = ArrayList<ChunkEntity>() + chunks.forEach { + if (it != currentChunk) { + Timber.d("Merge $it") + currentChunk.merge(roomId, it, direction) + chunksToDelete.add(it) + } + } + chunksToDelete.forEach { + it.deleteOnCascade() + } + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null + || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) + if (shouldUpdateSummary) { + val latestPreviewableEvent = TimelineEventEntity.latestEvent( + realm, + roomId, + includesSending = true, + filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES + ) + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent + } + if (currentChunk.isValid) { + RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..8707eb24294b2f312b43da174d78f93026c5734b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.tombstone + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + if (event.roomId == null) return + val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>() + if (createRoomContent?.replacementRoomId == null) return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + } + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.STATE_ROOM_TOMBSTONE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8db9ce69c622b83d1dc71cf1b32c13e067f5354 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import android.os.SystemClock +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber + +/** + * Rules: + * - user is typing: notify the homeserver (true), at least once every 10s + * - user stop typing: after 10s delay: notify the homeserver (false) + * - user empty the text composer or quit the timeline screen: notify the homeserver (false) + */ +internal class DefaultTypingService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val sendTypingTask: SendTypingTask +) : TypingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TypingService + } + + private var currentTask: Cancelable? = null + private var currentAutoStopTask: Cancelable? = null + + // What the homeserver knows + private var userIsTyping = false + // Last time the user is typing event has been sent + private var lastRequestTimestamp: Long = 0 + + override fun userIsTyping() { + scheduleAutoStop() + + val now = SystemClock.elapsedRealtime() + + if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) { + Timber.d("Typing: Skip start request") + return + } + + Timber.d("Typing: Send start request") + userIsTyping = true + lastRequestTimestamp = now + + currentTask?.cancel() + + val params = SendTypingTask.Params(roomId, true) + currentTask = sendTypingTask + .configureWith(params) + .executeBy(taskExecutor) + } + + override fun userStopsTyping() { + if (!userIsTyping) { + Timber.d("Typing: Skip stop request") + return + } + + Timber.d("Typing: Send stop request") + userIsTyping = false + lastRequestTimestamp = 0 + + currentAutoStopTask?.cancel() + currentTask?.cancel() + + val params = SendTypingTask.Params(roomId, false) + currentTask = sendTypingTask + .configureWith(params) + .executeBy(taskExecutor) + } + + private fun scheduleAutoStop() { + Timber.d("Typing: Schedule auto stop") + currentAutoStopTask?.cancel() + + val params = SendTypingTask.Params( + roomId, + false, + delay = MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS) + currentAutoStopTask = sendTypingTask + .configureWith(params) { + callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + userIsTyping = false + } + } + } + .executeBy(taskExecutor) + } + + companion object { + private const val MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS = 10_000L + private const val MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..719fffbb4ec6b18a56eda2e3505fc694691253a2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import kotlinx.coroutines.delay +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> { + + data class Params( + val roomId: String, + val isTyping: Boolean, + val typingTimeoutMillis: Int? = 30_000, + // Optional delay before sending the request to the homeserver + val delay: Long? = null + ) +} + +internal class DefaultSendTypingTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : SendTypingTask { + + override suspend fun execute(params: SendTypingTask.Params) { + delay(params.delay ?: -1) + + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.sendTypingState( + params.roomId, + userId, + TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping }) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..8ed77a4829f6e00b68265cb4a6c0cc56c991dcd8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TypingBody( + // Required. Whether the user is typing or not. If false, the timeout key can be omitted. + @Json(name = "typing") + val typing: Boolean, + // The length of time in milliseconds to mark this user as typing. + @Json(name = "timeout") + val timeout: Int? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..f616bfefd030aa3ada26d48a2cca30c59afb628b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TypingEventContent( + @Json(name = "user_ids") + val typingUserIds: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..76fb18b130bdd404466f8cefa557c5a76b86f7b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.uploads + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultUploadsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val getUploadsTask: GetUploadsTask, + private val cryptoService: CryptoService +) : UploadsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): UploadsService + } + + override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback<GetUploadsResult>): Cancelable { + return getUploadsTask + .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..be53b8afe180607379a06b3ec53bc72f71535762 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.uploads + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult +import org.matrix.android.sdk.api.session.room.uploads.UploadEvent +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterFactory +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetUploadsTask : Task<GetUploadsTask.Params, GetUploadsResult> { + + data class Params( + val roomId: String, + val isRoomEncrypted: Boolean, + val numberOfEvents: Int, + val since: String? + ) +} + +internal class DefaultGetUploadsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val tokenStore: SyncTokenStore, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus) + : GetUploadsTask { + + override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { + val result: GetUploadsResult + val events: List<Event> + + if (params.isRoomEncrypted) { + // Get a chunk of events from cache for e2e rooms + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = "", + hasMore = false + ) + + var eventsFromRealm = emptyList<Event>() + monarchy.doWithRealm { realm -> + eventsFromRealm = EventEntity.whereType(realm, EventType.ENCRYPTED, params.roomId) + .like(EventEntityFields.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.URL) + .findAll() + .map { it.asDomain() } + // Exclude stickers + .filter { it.getClearType() != EventType.STICKER } + } + events = eventsFromRealm + } else { + val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available") + + val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString() + val chunk = executeRequest<PaginationResponse>(eventBus) { + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) + } + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = chunk.end ?: "", + hasMore = chunk.hasMore() + ) + events = chunk.events + } + + var uploadEvents = listOf<UploadEvent>() + + val cacheOfSenderInfos = mutableMapOf<String, SenderInfo>() + + // Get a snapshot of all room members + monarchy.doWithRealm { realm -> + val roomMemberHelper = RoomMemberHelper(realm, params.roomId) + + uploadEvents = events.mapNotNull { event -> + val eventId = event.eventId ?: return@mapNotNull null + val messageContent = event.getClearContent()?.toModel<MessageContent>() ?: return@mapNotNull null + val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null + val senderId = event.senderId ?: return@mapNotNull null + + val senderInfo = cacheOfSenderInfos.getOrPut(senderId) { + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId) + SenderInfo( + userId = senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + + UploadEvent( + root = event, + eventId = eventId, + contentWithAttachmentContent = messageWithAttachmentContent, + senderInfo = senderInfo + ) + } + } + + return result.copy(uploadEvents = uploadEvents) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ac796791aed3b327ac89c6f0879615c5986d71f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { + + override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) { + secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream) + } + + override fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return secretStoringUtils.loadSecureSecret(inputStream, keyAlias) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d898e46ce38d70e04c3f8cca8d28b7cda6e6ab6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -0,0 +1,572 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.session.securestorage + +import android.content.Context +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.SecureRandom +import java.util.Calendar +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import javax.security.auth.x500.X500Principal + +/** + * Offers simple methods to securely store secrets in an Android Application. + * The encryption keys are randomly generated and securely managed by the key store, thus your secrets + * are safe. You only need to remember a key alias to perform encrypt/decrypt operations. + * + * <b>Android M++</b> + * On android M+, the keystore can generates and store AES keys via API. But below API M this functionality + * is not available. + * + * <b>Android [K-M[</b> + * For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a + * random secret key in generated to perform encryption. + * This secret key is encrypted with the public RSA key and stored with the encrypted secret. + * In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key. + * + * <b>Older androids</b> + * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt. + * The salt and iv are stored with encrypted data. + * + * Sample usage: + * <code> + * val secret = "The answer is 42" + * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context) + * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example + * + * //to get back the secret, just call + * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) + * </code> + * + * You can also just use this utility to store a secret key, and use any encryption algorithm that you want. + * + * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you + * add a pin or change the schema); So you might and with a useless pile of bytes. + */ +internal class SecretStoringUtils @Inject constructor(private val context: Context) { + + companion object { + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val AES_MODE = "AES/GCM/NoPadding" + private const val RSA_MODE = "RSA/ECB/PKCS1Padding" + + private const val FORMAT_API_M: Byte = 0 + private const val FORMAT_1: Byte = 1 + private const val FORMAT_2: Byte = 2 + } + + private val keyStore: KeyStore by lazy { + KeyStore.getInstance(ANDROID_KEY_STORE).apply { + load(null) + } + } + + private val secureRandom = SecureRandom() + + fun safeDeleteKey(keyAlias: String) { + try { + keyStore.deleteEntry(keyAlias) + } catch (e: KeyStoreException) { + Timber.e(e) + } + } + + /** + * Encrypt the given secret using the android Keystore. + * On android >= M, will directly use the keystore to generate a symmetric key + * On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated + * in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned + * in the bytearray (in can be stored anywhere, it is encrypted) + * On older version a key in generated from alias with random salt. + * + * The secret is encrypted using the following method: AES/GCM/NoPadding + */ + @Throws(Exception::class) + fun securelyStoreString(secret: String, keyAlias: String): ByteArray? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias) + else -> encryptForOldDevicesNotGood(secret, keyAlias) + } + } + + /** + * Decrypt a secret that was encrypted by #securelyStoreString() + */ + @Throws(Exception::class) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias) + else -> decryptForOldDevicesNotGood(encrypted, keyAlias) + } + } + + fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any) + else -> saveSecureObjectOldNotGood(keyAlias, output, any) + } + } + + fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream) + else -> loadSecureObjectOldNotGood(keyAlias, inputStream) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + if (secretKeyEntry == null) { + // we generate it + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + val keyGenSpec = KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(128) + .build() + generator.init(keyGenSpec) + return generator.generateKey() + } + return secretKeyEntry + } + + /* + Symmetric Key Generation is only available in M, so before M the idea is to: + - Generate a pair of RSA keys; + - Generate a random AES key; + - Encrypt the AES key using the RSA public key; + - Store the encrypted AES + Generate a key pair for encryption + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { + val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) + + if (privateKeyEntry != null) return privateKeyEntry + + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 30) + + val spec = KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(X500Principal("CN=$alias")) + .setSerialNumber(BigInteger.TEN) + // .setEncryptionRequired() requires that the phone as a pin/schema + .setStartDate(start.time) + .setEndDate(end.time) + .build() + KeyPairGenerator.getInstance("RSA" /*KeyProperties.KEY_ALGORITHM_RSA*/, ANDROID_KEY_STORE).run { + initialize(spec) + generateKeyPair() + } + return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry) + } + + @RequiresApi(Build.VERSION_CODES.M) + fun encryptStringM(text: String, keyAlias: String): ByteArray? { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + // we happen the iv to the final result + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + return formatMMake(iv, encryptedBytes) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { + val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk)) + + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + return String(cipher.doFinal(encryptedText), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun encryptStringK(text: String, keyAlias: String): ByteArray? { + // we generate a random symmetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + // we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format1Make(encryptedKey, iv, encryptedBytes) + } + + private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format2Make(salt, iv, encryptedBytes) + } + + private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? { + val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data)) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) +// cipher.init(Cipher.ENCRYPT_MODE, sKey) +// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, specIV) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun decryptStringK(data: ByteArray, keyAlias: String): String? { + val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) + + // we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + // Have to do it like that if i encapsulate the output stream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + output.write(FORMAT_API_M.toInt()) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) { + // we generate a random symmetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + // we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + val cos = CipherOutputStream(bos1, cipher) + ObjectOutputStream(cos).use { + it.writeObject(writeObject) + } + + output.write(FORMAT_1.toInt()) + output.write((encryptedKey.size and 0xFF00).shr(8)) + output.write(encryptedKey.size and 0x00FF) + output.write(encryptedKey) + output.write(iv.size) + output.write(iv) + output.write(bos1.toByteArray()) + } + + private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val secretKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + // Have to do it like that if i encapsulate the output stream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + + output.write(FORMAT_2.toInt()) + output.write(salt.size) + output.write(salt) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) { +// FileOutputStream(file).use { +// saveSecureObjectM(keyAlias, it, writeObject) +// } +// } +// +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? { +// FileInputStream(file).use { +// return loadSecureObjectM<T>(keyAlias, it) +// } +// } + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val format = inputStream.read() + assert(format.toByte() == FORMAT_API_M) + + val ivSize = inputStream.read() + val iv = ByteArray(ivSize) + inputStream.read(iv, 0, ivSize) + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + CipherInputStream(inputStream, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(IOException::class) + private fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? { + val (encryptedKey, iv, encrypted) = format1Extract(inputStream) + + // we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @Throws(Exception::class) + private fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { + val (salt, iv, encrypted) = format2Extract(inputStream) + + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val sKey = SecretKeySpec(tmp.encoded, "AES") + // we need to decrypt the key + + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { + ObjectInputStream(it).use { ois -> + val readObject = ois.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(Exception::class) + private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) + // Encrypt the text + val inputCipher = Cipher.getInstance(RSA_MODE) + inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) + + val outputStream = ByteArrayOutputStream() + CipherOutputStream(outputStream, inputCipher).use { + it.write(secret) + } + + return outputStream.toByteArray() + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(Exception::class) + private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) + val output = Cipher.getInstance(RSA_MODE) + output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) + + return CipherInputStream(encrypted, output).use { it.readBytes() } + } + + private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> { + val format = bis.read().toByte() + assert(format == FORMAT_API_M) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv, 0, ivSize) + + val encrypted = bis.readBytes() + return Pair(iv, encrypted) + } + + private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(2 + iv.size + data.size) + bos.write(FORMAT_API_M.toInt()) + bos.write(iv.size) + bos.write(iv) + bos.write(data) + return bos.toByteArray() + } + + private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> { + val format = bis.read() + assert(format.toByte() == FORMAT_1) + + val keySizeBig = bis.read() + val keySizeLow = bis.read() + val encryptedKeySize = keySizeBig.shl(8) + keySizeLow + val encryptedKey = ByteArray(encryptedKeySize) + bis.read(encryptedKey) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val encrypted = bis.readBytes() + return Triple(encryptedKey, iv, encrypted) + } + + private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_1.toInt()) + bos.write((encryptedKey.size and 0xFF00).shr(8)) + bos.write(encryptedKey.size and 0x00FF) + bos.write(encryptedKey) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_2.toInt()) + bos.write(salt.size) + bos.write(salt) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> { + val format = bis.read() + assert(format.toByte() == FORMAT_2) + + val saltSize = bis.read() + val salt = ByteArray(saltSize) + bis.read(salt) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val encrypted = bis.readBytes() + return Triple(salt, iv, encrypted) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0fdecc8d211d9b67459e90c2f90403edb9690533 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import javax.inject.Inject + +internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, + private val signInAgainTask: SignInAgainTask, + private val sessionParamsStore: SessionParamsStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor) : SignOutService { + + override fun signInAgain(password: String, + callback: MatrixCallback<Unit>): Cancelable { + return signInAgainTask + .configureWith(SignInAgainTask.Params(password)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateCredentials(credentials: Credentials, + callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + sessionParamsStore.updateCredentials(credentials) + } + } + + override fun signOut(signOutFromHomeserver: Boolean, + callback: MatrixCallback<Unit>): Cancelable { + return signOutTask + .configureWith(SignOutTask.Params(signOutFromHomeserver)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..6f26fb25cd6364c731ba8f8620c1c501da171fde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> { + data class Params( + val password: String + ) +} + +internal class DefaultSignInAgainTask @Inject constructor( + private val signOutAPI: SignOutAPI, + private val sessionParams: SessionParams, + private val sessionParamsStore: SessionParamsStore, + private val eventBus: EventBus +) : SignInAgainTask { + + override suspend fun execute(params: SignInAgainTask.Params) { + val newCredentials = executeRequest<Credentials>(eventBus) { + apiCall = signOutAPI.loginAgain( + PasswordLoginParams.userIdentifier( + // Reuse the same userId + sessionParams.userId, + params.password, + // The spec says the initial device name will be ignored + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + // but https://github.com/matrix-org/synapse/issues/6525 + // Reuse the same deviceId + deviceId = sessionParams.deviceId + ) + ) + } + + sessionParamsStore.updateCredentials(newCredentials) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4b05bfc6722a644cd5b5063d5b2c6d81d3c6701 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +internal interface SignOutAPI { + + /** + * Attempt to login again to the same account. + * Set all the timeouts to 1 minute + * It is similar to [AuthAPI.login] + * + * @param loginParams the login parameters + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials> + + /** + * Invalidate the access token, so that it can no longer be used for authorization. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout") + fun signOut(): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4822000305220494833199aa5952f8f7f9883e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.signout + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class SignOutModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSignOutAPI(retrofit: Retrofit): SignOutAPI { + return retrofit.create(SignOutAPI::class.java) + } + } + + @Binds + abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask + + @Binds + abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask + + @Binds + abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef507477cd25eff85c5f42fd3422cf493757d75d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.cleanup.CleanupSession +import org.matrix.android.sdk.internal.session.identity.IdentityDisconnectTask +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import java.net.HttpURLConnection +import javax.inject.Inject + +internal interface SignOutTask : Task<SignOutTask.Params, Unit> { + data class Params( + val signOutFromHomeserver: Boolean + ) +} + +internal class DefaultSignOutTask @Inject constructor( + private val signOutAPI: SignOutAPI, + private val eventBus: EventBus, + private val identityDisconnectTask: IdentityDisconnectTask, + private val cleanupSession: CleanupSession +) : SignOutTask { + + override suspend fun execute(params: SignOutTask.Params) { + // It should be done even after a soft logout, to be sure the deviceId is deleted on the + if (params.signOutFromHomeserver) { + Timber.d("SignOut: send request...") + try { + executeRequest<Unit>(eventBus) { + apiCall = signOutAPI.signOut() + } + } catch (throwable: Throwable) { + // Maybe due to https://github.com/matrix-org/synapse/issues/5756 + if (throwable is Failure.ServerError + && throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) { + // Also throwable.error.isSoftLogout should be true + // Ignore + Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") + } else { + throw throwable + } + } + } + + // Logout from identity server if any + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + Timber.d("SignOut: cleanup session...") + cleanupSession.handle() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..5c855e190cba30dff0c24f0f322ba1fec8db27d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse +import timber.log.Timber +import javax.inject.Inject + +internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, + private val verificationService: DefaultVerificationService) { + + fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) { + val total = toDevice.events?.size ?: 0 + toDevice.events?.forEachIndexed { index, event -> + initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) + // Decrypt event if necessary + decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE + && event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { + Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") + } else { + verificationService.onToDeviceEvent(event) + cryptoService.onToDeviceEvent(event) + } + } + } + + fun onSyncCompleted(syncResponse: SyncResponse) { + cryptoService.onSyncCompleted(syncResponse) + } + + /** + * Decrypt an encrypted event + * + * @param event the event to decrypt + * @param timelineId the timeline identifier + * @return true if the event has been decrypted + */ + private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { + Timber.v("## CRYPTO | decryptToDeviceEvent") + if (event.getClearType() == EventType.ENCRYPTED) { + var result: MXEventDecryptionResult? = null + try { + result = cryptoService.decryptEvent(event, timelineId ?: "") + } catch (exception: MXCryptoError) { + event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) + Timber.e("## CRYPTO | Failed to decrypt to device event: ${event.mCryptoError ?: exception}") + } + + if (null != result) { + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + return true + } + } + + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3dd9fd577b4179a6d7f614d56ec8a8e1883e687 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.mapWithProgress +import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.InvitedGroupSync +import io.realm.Realm +import javax.inject.Inject + +internal class GroupSyncHandler @Inject constructor() { + + sealed class HandlingStrategy { + data class JOINED(val data: Map<String, Any>) : HandlingStrategy() + data class INVITED(val data: Map<String, InvitedGroupSync>) : HandlingStrategy() + data class LEFT(val data: Map<String, Any>) : HandlingStrategy() + } + + fun handle( + realm: Realm, + roomsSyncResponse: GroupsSyncResponse, + reporter: DefaultInitialSyncProgressService? = null + ) { + handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) + handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) + handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) + } + + // PRIVATE METHODS ***************************************************************************** + + private fun handleGroupSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) { + val groups = when (handlingStrategy) { + is HandlingStrategy.JOINED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.6f) { + handleJoinedGroup(realm, it.key) + } + + is HandlingStrategy.INVITED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.3f) { + handleInvitedGroup(realm, it.key) + } + + is HandlingStrategy.LEFT -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.1f) { + handleLeftGroup(realm, it.key) + } + } + realm.insertOrUpdate(groups) + } + + private fun handleJoinedGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.JOIN + groupSummaryEntity.membership = Membership.JOIN + return groupEntity + } + + private fun handleInvitedGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.INVITE + groupSummaryEntity.membership = Membership.INVITE + return groupEntity + } + + private fun handleLeftGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.LEAVE + groupSummaryEntity.membership = Membership.LEAVE + return groupEntity + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..5fe23e1b691ab0428b8a4f942cf98d84a2f22d44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.query.createUnmanaged +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +// the receipts dictionaries +// key : $EventId +// value : dict key $UserId +// value dict key ts +// dict value ts value +typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>> + +private const val READ_KEY = "m.read" +private const val TIMESTAMP_KEY = "ts" + +internal class ReadReceiptHandler @Inject constructor() { + + companion object { + + fun createContent(userId: String, eventId: String): ReadReceiptContent { + return mapOf( + eventId to mapOf( + READ_KEY to mapOf( + userId to mapOf( + TIMESTAMP_KEY to System.currentTimeMillis().toDouble() + ) + ) + ) + ) + } + } + + fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { + if (content == null) { + return + } + try { + handleReadReceiptContent(realm, roomId, content, isInitialSync) + } catch (exception: Exception) { + Timber.e("Fail to handle read receipt for room $roomId") + } + } + + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) { + if (isInitialSync) { + initialSyncStrategy(realm, roomId, content) + } else { + incrementalSyncStrategy(realm, roomId, content) + } + } + + private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>() + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) + readReceiptsSummary.readReceipts.add(receiptEntity) + } + readReceiptSummaries.add(readReceiptsSummary) + } + realm.insertOrUpdate(readReceiptSummaries) + } + + private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + // ensure new ts is superior to the previous one + if (ts > receiptEntity.originServerTs) { + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) + } + receiptEntity.eventId = eventId + receiptEntity.originServerTs = ts + readReceiptsSummary.readReceipts.add(receiptEntity) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0c44ef4f0b30f994429758819097698b8a456d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.session.room.read.FullyReadContent +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class RoomFullyReadHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: FullyReadContent?) { + if (content == null) { + return + } + Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + + RoomSummaryEntity.getOrCreate(realm, roomId).apply { + readMarkerId = content.eventId + } + ReadMarkerEntity.getOrCreate(realm, roomId).apply { + this.eventId = content.eventId + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..64c30825fc74dab078b3df66a382050985e182e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -0,0 +1,430 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.helper.addOrUpdate +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.deleteOnCascade +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.mapWithProgress +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler +import org.matrix.android.sdk.internal.session.room.read.FullyReadContent +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent +import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.internal.session.sync.model.RoomSync +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomTagHandler: RoomTagHandler, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val cryptoService: DefaultCryptoService, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + @UserId private val userId: String, + private val eventBus: EventBus, + private val timelineEventDecryptor: TimelineEventDecryptor) { + + sealed class HandlingStrategy { + data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy() + data class INVITED(val data: Map<String, InvitedRoomSync>) : HandlingStrategy() + data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy() + } + + fun handle( + realm: Realm, + roomsSyncResponse: RoomsSyncResponse, + isInitialSync: Boolean, + reporter: DefaultInitialSyncProgressService? = null + ) { + Timber.v("Execute transaction from $this") + handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) + } + + // PRIVATE METHODS ***************************************************************************** + + private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { + val insertType = if (isInitialSync) { + EventInsertType.INITIAL_SYNC + } else { + EventInsertType.INCREMENTAL_SYNC + } + val syncLocalTimeStampMillis = System.currentTimeMillis() + val rooms = when (handlingStrategy) { + is HandlingStrategy.JOINED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { + handleJoinedRoom(realm, it.key, it.value, isInitialSync, insertType, syncLocalTimeStampMillis) + } + is HandlingStrategy.INVITED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { + handleInvitedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) + } + + is HandlingStrategy.LEFT -> { + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) { + handleLeftRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) + } + } + } + realm.insertOrUpdate(rooms) + } + + private fun handleJoinedRoom(realm: Realm, + roomId: String, + roomSync: RoomSync, + isInitialSync: Boolean, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + Timber.v("Handle join sync for room $roomId") + + var ephemeralResult: EphemeralResult? = null + if (roomSync.ephemeral?.events?.isNotEmpty() == true) { + ephemeralResult = handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync) + } + + if (roomSync.accountData?.events?.isNotEmpty() == true) { + handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) + } + + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + + if (roomEntity.membership == Membership.INVITE) { + roomEntity.chunks.deleteAllFromRealm() + } + roomEntity.membership = Membership.JOIN + + // State event + if (roomSync.state?.events?.isNotEmpty() == true) { + for (event in roomSync.state.events) { + if (event.eventId == null || event.stateKey == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + // Give info to crypto module + cryptoService.onStateEvent(roomId, event) + roomMemberEventHandler.handle(realm, roomId, event) + } + } + if (roomSync.timeline?.events?.isNotEmpty() == true) { + val chunkEntity = handleTimelineEvents( + realm, + roomId, + roomEntity, + roomSync.timeline.events, + roomSync.timeline.prevToken, + roomSync.timeline.limited, + insertType, + syncLocalTimestampMillis, + isInitialSync + ) + roomEntity.addOrUpdate(chunkEntity) + } + val hasRoomMember = roomSync.state?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null || roomSync.timeline?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null + + roomTypingUsersHandler.handle(realm, roomId, ephemeralResult) + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN) + roomSummaryUpdater.update( + realm, + roomId, + Membership.JOIN, + roomSync.summary, + roomSync.unreadNotifications, + updateMembers = hasRoomMember + ) + return roomEntity + } + + private fun handleInvitedRoom(realm: Realm, + roomId: String, + roomSync: InvitedRoomSync, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + Timber.v("Handle invited sync for room $roomId") + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + roomEntity.membership = Membership.INVITE + if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { + roomSync.inviteState.events.forEach { event -> + if (event.stateKey == null) { + return@forEach + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = eventEntity.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, event) + } + } + val inviterEvent = roomSync.inviteState?.events?.lastOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId) + return roomEntity + } + + private fun handleLeftRoom(realm: Realm, + roomId: String, + roomSync: RoomSync, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + for (event in roomSync.state?.events.orEmpty()) { + if (event.eventId == null || event.stateKey == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, event) + } + for (event in roomSync.timeline?.events.orEmpty()) { + if (event.eventId == null || event.senderId == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + roomMemberEventHandler.handle(realm, roomEntity.roomId, event) + } + } + } + val leftMember = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst() + val membership = leftMember?.membership ?: Membership.LEAVE + roomEntity.membership = membership + roomEntity.chunks.deleteAllFromRealm() + roomTypingUsersHandler.handle(realm, roomId, null) + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) + roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications) + return roomEntity + } + + private fun handleTimelineEvents(realm: Realm, + roomId: String, + roomEntity: RoomEntity, + eventList: List<Event>, + prevToken: String? = null, + isLimited: Boolean = true, + insertType: EventInsertType, + syncLocalTimestampMillis: Long, + isInitialSync: Boolean): ChunkEntity { + val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + val chunkEntity = if (!isLimited && lastChunk != null) { + lastChunk + } else { + realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken } + } + // Only one chunk has isLastForward set to true + lastChunk?.isLastForward = false + chunkEntity.isLastForward = true + + val eventIds = ArrayList<String>(eventList.size) + val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() + + for (event in eventList) { + if (event.eventId == null || event.senderId == null) { + continue + } + eventIds.add(event.eventId) + + if (event.isEncrypted() && !isInitialSync) { + decryptIfNeeded(event, roomId) + } + + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + val fixedContent = event.getFixedRoomMemberContent() + roomMemberContentsByUser[event.stateKey] = fixedContent + roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent) + } + } + roomMemberContentsByUser.getOrPut(event.senderId) { + // If we don't have any new state on this user, get it from db + val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root + rootStateEvent?.asDomain()?.getFixedRoomMemberContent() + } + + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + // Give info to crypto module + cryptoService.onLiveEvent(roomEntity.roomId, event) + + // Try to remove local echo + event.unsignedData?.transactionId?.also { + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + if (sendingEventEntity != null) { + Timber.v("Remove local echo for tx:$it") + roomEntity.sendingTimelineEvents.remove(sendingEventEntity) + if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { + // updated with echo decryption, to avoid seeing it decrypt again + val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) + sendingEventEntity.root?.decryptionResultJson?.let { json -> + eventEntity.decryptionResultJson = json + event.mxDecryptionResult = adapter.fromJson(json) + } + } + // Finally delete the local echo + sendingEventEntity.deleteOnCascade() + } else { + Timber.v("Can't find corresponding local echo for tx:$it") + } + } + } + // posting new events to timeline if any is registered + eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds)) + return chunkEntity + } + + private fun decryptIfNeeded(event: Event, roomId: String) { + try { + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } + + data class EphemeralResult( + val typingUserIds: List<String> = emptyList() + ) + + private fun handleEphemeral(realm: Realm, + roomId: String, + ephemeral: RoomSyncEphemeral, + isInitialSync: Boolean): EphemeralResult { + var result = EphemeralResult() + for (event in ephemeral.events) { + when (event.type) { + EventType.RECEIPT -> { + @Suppress("UNCHECKED_CAST") + (event.content as? ReadReceiptContent)?.let { readReceiptContent -> + readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync) + } + } + EventType.TYPING -> { + event.content.toModel<TypingEventContent>()?.let { typingEventContent -> + result = result.copy(typingUserIds = typingEventContent.typingUserIds) + } + } + else -> Timber.w("Ephemeral event type '${event.type}' not yet supported") + } + } + + return result + } + + private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { + for (event in accountData.events) { + val eventType = event.getClearType() + if (eventType == EventType.TAG) { + val content = event.getClearContent().toModel<RoomTagContent>() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == EventType.FULLY_READ) { + val content = event.getClearContent().toModel<FullyReadContent>() + roomFullyReadHandler.handle(realm, roomId, content) + } + } + } + + private fun Event.getFixedRoomMemberContent(): RoomMemberContent? { + val content = content.toModel<RoomMemberContent>() + // if user is leaving, we should grab his last name and avatar from prevContent + return if (content?.membership?.isLeft() == true) { + val prevContent = resolvedPrevContent().toModel<RoomMemberContent>() + content.copy( + displayName = prevContent?.displayName, + avatarUrl = prevContent?.avatarUrl + ) + } else { + content + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..8dbd77f3fd32cfae6a570b043f070eaadded6bec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomTagEntity +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTagHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: RoomTagContent?) { + if (content == null) { + return + } + val tags = content.tags.entries.map { (tagName, params) -> + RoomTagEntity(tagName, params["order"] as? Double) + } + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: RoomSummaryEntity(roomId) + + roomSummaryEntity.tags.clear() + roomSummaryEntity.tags.addAll(tags) + realm.insertOrUpdate(roomSummaryEntity) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..71a4d33d3f4c56d89b70da3100b42e8d29b67185 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String, + private val typingUsersTracker: DefaultTypingUsersTracker) { + + fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList() + val senderInfo = typingIds.map { userId -> + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) + SenderInfo( + userId = userId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + typingUsersTracker.setTypingUsersFromRoom(roomId, senderInfo) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..db14c53456b94a83f4866d1d137df9fa82d97359 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.QueryMap + +internal interface SyncAPI { + + /** + * Set all the timeouts to 1 minute + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync") + fun sync(@QueryMap params: Map<String, String>): Call<SyncResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..87afe78e701cf166abed88c2c95f8785b1012ac6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class SyncModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSyncAPI(retrofit: Retrofit): SyncAPI { + return retrofit.create(SyncAPI::class.java) + } + } + + @Binds + abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..b58727cbaa5e2bfde8116d9c2d4f18354ec96ef8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import androidx.work.ExistingPeriodicWorkPolicy +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker +import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask +import org.matrix.android.sdk.internal.session.reportSubtask +import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" + +internal class SyncResponseHandler @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, + private val roomSyncHandler: RoomSyncHandler, + private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val groupSyncHandler: GroupSyncHandler, + private val cryptoSyncHandler: CryptoSyncHandler, + private val cryptoService: DefaultCryptoService, + private val tokenStore: SyncTokenStore, + private val processEventForPushTask: ProcessEventForPushTask, + private val pushRuleService: PushRuleService, + private val initialSyncProgressService: DefaultInitialSyncProgressService) { + + suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?) { + val isInitialSync = fromToken == null + Timber.v("Start handling sync, is InitialSync: $isInitialSync") + val reporter = initialSyncProgressService.takeIf { isInitialSync } + + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start() + } + cryptoService.onSyncWillProcess(isInitialSync) + }.also { + Timber.v("Finish handling start cryptoService in $it ms") + } + + // Handle the to device events before the room ones + // to ensure to decrypt them properly + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + // Start one big transaction + monarchy.awaitTransaction { realm -> + measureTimeMillis { + Timber.v("Handle rooms") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { + if (syncResponse.rooms != null) { + roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter) + } + } + }.also { + Timber.v("Finish handling rooms in $it ms") + } + + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) { + Timber.v("Handle groups") + if (syncResponse.groups != null) { + groupSyncHandler.handle(realm, syncResponse.groups, reporter) + } + } + }.also { + Timber.v("Finish handling groups in $it ms") + } + + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { + Timber.v("Handle accountData") + userAccountDataSyncHandler.handle(realm, syncResponse.accountData) + } + }.also { + Timber.v("Finish handling accountData in $it ms") + } + tokenStore.saveToken(realm, syncResponse.nextBatch) + } + // Everything else we need to do outside the transaction + syncResponse.rooms?.let { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + } + syncResponse.groups?.let { + scheduleGroupDataFetchingIfNeeded(it) + } + + Timber.v("On sync completed") + cryptoSyncHandler.onSyncCompleted(syncResponse) + } + + /** + * At the moment we don't get any group data through the sync, so we poll where every hour. + You can also force to refetch group data using [Group] API. + */ + private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) { + val groupIds = ArrayList<String>() + groupIds.addAll(groupsSyncResponse.join.keys) + groupIds.addAll(groupsSyncResponse.invite.keys) + if (groupIds.isEmpty()) { + Timber.v("No new groups to fetch data for.") + return + } + Timber.v("There are ${groupIds.size} new groups to fetch data for.") + val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId) + val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) + + val getGroupWork = workManagerProvider.matrixPeriodicWorkRequestBuilder<GetGroupDataWorker>(1, TimeUnit.HOURS) + .setInputData(workData) + .setConstraints(WorkManagerProvider.workConstraints) + .build() + + workManagerProvider.workManager + .enqueueUniquePeriodicWork(GET_GROUP_DATA_WORKER, ExistingPeriodicWorkPolicy.REPLACE, getGroupWork) + } + + private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) { + Timber.v("[PushRules] --> checkPushRules") + if (isInitialSync) { + Timber.v("[PushRules] <-- No push rule check on initial sync") + return + } // nothing on initial sync + + val rules = pushRuleService.getPushRules(RuleScope.GLOBAL).getAllRules() + processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) + Timber.v("[PushRules] <-- Push task scheduled") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..02afd53908a7516d783dbe27febb42ddd60e0e55 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.session.user.UserStore +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface SyncTask : Task<SyncTask.Params, Unit> { + + data class Params(var timeout: Long = 30_000L) +} + +internal class DefaultSyncTask @Inject constructor( + private val syncAPI: SyncAPI, + @UserId private val userId: String, + private val filterRepository: FilterRepository, + private val syncResponseHandler: SyncResponseHandler, + private val initialSyncProgressService: DefaultInitialSyncProgressService, + private val syncTokenStore: SyncTokenStore, + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, + private val userStore: UserStore, + private val syncTaskSequencer: SyncTaskSequencer, + private val eventBus: EventBus +) : SyncTask { + + override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post { + doSync(params) + } + + private suspend fun doSync(params: SyncTask.Params) { + Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") + + val requestParams = HashMap<String, String>() + var timeout = 0L + val token = syncTokenStore.getLastToken() + if (token != null) { + requestParams["since"] = token + timeout = params.timeout + } + requestParams["timeout"] = timeout.toString() + requestParams["filter"] = filterRepository.getFilter() + + val isInitialSync = token == null + if (isInitialSync) { + // We might want to get the user information in parallel too + userStore.createOrUpdate(userId) + initialSyncProgressService.endAll() + initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) + } + // Maybe refresh the home server capabilities data we know + getHomeServerCapabilitiesTask.execute(Unit) + + val syncResponse = executeRequest<SyncResponse>(eventBus) { + apiCall = syncAPI.sync(requestParams) + } + syncResponseHandler.handleResponse(syncResponse, token) + if (isInitialSync) { + initialSyncProgressService.endAll() + } + Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt new file mode 100644 index 0000000000000000000000000000000000000000..4b29a82ad4af55aca738a9353b20a537e1c12b35 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import javax.inject.Inject + +@SessionScope +internal class SyncTaskSequencer @Inject constructor() : SemaphoreCoroutineSequencer() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..e001e61149bd02429e4572279de044f9e365b7ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.SyncEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import javax.inject.Inject + +internal class SyncTokenStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getLastToken(): String? { + return Realm.getInstance(monarchy.realmConfiguration).use { + it.where(SyncEntity::class.java).findFirst()?.nextBatch + } + } + + fun saveToken(realm: Realm, token: String?) { + val sync = SyncEntity(token) + realm.insertOrUpdate(sync) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ef6a5a3e1c1a84171c94cd5912e242bb6cda826 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync + +import com.squareup.moshi.Moshi +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields +import org.matrix.android.sdk.internal.database.query.getDirectRooms +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync +import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.where +import timber.log.Timber +import javax.inject.Inject + +internal class UserAccountDataSyncHandler @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String, + private val directChatsHelper: DirectChatsHelper, + private val moshi: Moshi, + private val updateUserAccountDataTask: UpdateUserAccountDataTask) { + + fun handle(realm: Realm, accountData: UserAccountDataSync?) { + accountData?.list?.forEach { event -> + // Generic handling, just save in base + handleGenericAccountData(realm, event.type, event.content) + when (event.type) { + UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) + UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) + UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) + UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) + } + } + } + + // If we get some direct chat invites, we synchronize the user account data including those. + suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) { + if (invites.isNullOrEmpty()) return + val directChats = directChatsHelper.getLocalUserAccount() + var hasUpdate = false + monarchy.doWithRealm { realm -> + invites.forEach { (roomId, _) -> + val myUserStateEvent = RoomMemberHelper(realm, roomId).getLastStateEvent(userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (inviterId != null && inviterId != userId && isDirect == true) { + directChats + .getOrPut(inviterId, { arrayListOf() }) + .apply { + if (contains(roomId)) { + Timber.v("Direct chats already include room $roomId with user $inviterId") + } else { + add(roomId) + hasUpdate = true + } + } + } + } + } + if (hasUpdate) { + val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( + directMessages = directChats + ) + updateUserAccountDataTask.execute(updateUserAccountParams) + } + } + + private fun handlePushRules(realm: Realm, event: UserAccountDataEvent) { + val pushRules = event.content.toModel<GetPushRulesResponse>() ?: return + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + + private fun handleDirectChatRooms(realm: Realm, event: UserAccountDataEvent) { + val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + oldDirectRooms.forEach { + it.isDirect = false + it.directUserId = null + } + val content = event.content.toModel<DirectMessagesContent>() ?: return + content.forEach { + val userId = it.key + it.value.forEach { roomId -> + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummaryEntity != null) { + roomSummaryEntity.isDirect = true + roomSummaryEntity.directUserId = userId + realm.insertOrUpdate(roomSummaryEntity) + } + } + } + } + + private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) { + val userIds = event.content.toModel<IgnoredUsersContent>()?.ignoredUsers?.keys ?: return + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + // And save the new received list + userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } + // TODO If not initial sync, we should execute a init sync + } + + private fun handleBreadcrumbs(realm: Realm, event: UserAccountDataEvent) { + val recentRoomIds = event.content.toModel<BreadcrumbsContent>()?.recentRoomIds ?: return + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList<String>().apply { addAll(recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } + } + + fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + val existing = realm.where<UserAccountDataEntity>() + .equalTo(UserAccountDataEntityFields.TYPE, type) + .findFirst() + if (existing != null) { + // Update current value + existing.contentStr = ContentMapper.map(content) + } else { + realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity -> + accountDataEntity.type = type + accountDataEntity.contentStr = ContentMapper.map(content) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt new file mode 100644 index 0000000000000000000000000000000000000000..20aa4093365870acfe6a57d61e0a5402661f69ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.job + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Can execute periodic sync task. + * An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver + * in order to be able to perform a sync even if the app is not running. + * The <receiver> and <service> must be declared in the Manifest or the app using the SDK + */ +abstract class SyncService : Service() { + + private var sessionId: String? = null + private var mIsSelfDestroyed: Boolean = false + + private var isInitialSync: Boolean = false + private lateinit var session: Session + private lateinit var syncTask: SyncTask + private lateinit var networkConnectivityChecker: NetworkConnectivityChecker + private lateinit var taskExecutor: TaskExecutor + private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers + private lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + + private val isRunning = AtomicBoolean(false) + + private val serviceScope = CoroutineScope(SupervisorJob()) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.i("onStartCommand $intent") + val isInit = initialize(intent) + if (isInit) { + onStart(isInitialSync) + doSyncIfNotAlreadyRunning() + } else { + // We should start and stop as we have to ensure to call Service.startForeground() + onStart(isInitialSync) + stopMe() + } + // No intent just start the service, an alarm will should call with intent + return START_STICKY + } + + override fun onDestroy() { + Timber.i("## onDestroy() : $this") + if (!mIsSelfDestroyed) { + Timber.w("## Destroy by the system : $this") + } + serviceScope.coroutineContext.cancelChildren() + isRunning.set(false) + super.onDestroy() + } + + private fun stopMe() { + mIsSelfDestroyed = true + stopSelf() + } + + private fun doSyncIfNotAlreadyRunning() { + if (isRunning.get()) { + Timber.i("Received a start while was already syncing... ignore") + } else { + isRunning.set(true) + serviceScope.launch(coroutineDispatchers.io) { + doSync() + } + } + } + + private suspend fun doSync() { + Timber.v("Execute sync request with timeout 0") + val params = SyncTask.Params(TIME_OUT) + try { + syncTask.execute(params) + // Start sync if we were doing an initial sync and the syncThread is not launched yet + if (isInitialSync && session.getSyncState() == SyncState.Idle) { + val isForeground = !backgroundDetectionObserver.isInBackground + session.startSync(isForeground) + } + stopMe() + } catch (throwable: Throwable) { + Timber.e(throwable) + if (throwable.isTokenError()) { + stopMe() + } else { + Timber.v("Should be rescheduled to avoid wasting resources") + sessionId?.also { + onRescheduleAsked(it, isInitialSync, delay = 10_000L) + } + stopMe() + } + } + } + + private fun initialize(intent: Intent?): Boolean { + if (intent == null) { + return false + } + val matrix = Matrix.getInstance(applicationContext) + val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false + try { + val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) + ?: throw IllegalStateException("You should have a session to make it work") + session = sessionComponent.session() + sessionId = safeSessionId + syncTask = sessionComponent.syncTask() + isInitialSync = !session.hasAlreadySynced() + networkConnectivityChecker = sessionComponent.networkConnectivityChecker() + taskExecutor = sessionComponent.taskExecutor() + coroutineDispatchers = sessionComponent.coroutineDispatchers() + backgroundDetectionObserver = matrix.backgroundDetectionObserver + return true + } catch (exception: Exception) { + Timber.e(exception, "An exception occurred during initialisation") + return false + } + } + + abstract fun onStart(isInitialSync: Boolean) + + abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + companion object { + const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" + private const val TIME_OUT = 0L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a2d6b1fd32f3ad337a4557551789a7f579ac5bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.job + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.squareup.moshi.JsonEncodingException +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.util.createUIHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.net.SocketTimeoutException +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject +import kotlin.concurrent.schedule + +private const val RETRY_WAIT_TIME_MS = 10_000L +private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L + +internal class SyncThread @Inject constructor(private val syncTask: SyncTask, + private val typingUsersTracker: DefaultTypingUsersTracker, + private val networkConnectivityChecker: NetworkConnectivityChecker, + private val backgroundDetectionObserver: BackgroundDetectionObserver) + : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { + + private var state: SyncState = SyncState.Idle + private var liveState = MutableLiveData<SyncState>(state) + private val lock = Object() + private val syncScope = CoroutineScope(SupervisorJob()) + private val debouncer = Debouncer(createUIHandler()) + + private var canReachServer = true + private var isStarted = false + private var isTokenValid = true + private var retryNoNetworkTask: TimerTask? = null + + init { + updateStateTo(SyncState.Idle) + } + + fun setInitialForeground(initialForeground: Boolean) { + val newState = if (initialForeground) SyncState.Idle else SyncState.Paused + updateStateTo(newState) + } + + fun restart() = synchronized(lock) { + if (!isStarted) { + Timber.v("Resume sync...") + isStarted = true + // Check again server availability and the token validity + canReachServer = true + isTokenValid = true + lock.notify() + } + } + + fun pause() = synchronized(lock) { + if (isStarted) { + Timber.v("Pause sync...") + isStarted = false + retryNoNetworkTask?.cancel() + syncScope.coroutineContext.cancelChildren() + } + } + + fun kill() = synchronized(lock) { + Timber.v("Kill sync...") + updateStateTo(SyncState.Killing) + retryNoNetworkTask?.cancel() + syncScope.coroutineContext.cancelChildren() + lock.notify() + } + + fun currentState() = state + + fun liveState(): LiveData<SyncState> { + return liveState + } + + override fun onConnectivityChanged() { + retryNoNetworkTask?.cancel() + synchronized(lock) { + canReachServer = true + lock.notify() + } + } + + override fun run() { + Timber.v("Start syncing...") + isStarted = true + networkConnectivityChecker.register(this) + backgroundDetectionObserver.register(this) + while (state != SyncState.Killing) { + Timber.v("Entering loop, state: $state") + if (!isStarted) { + Timber.v("Sync is Paused. Waiting...") + updateStateTo(SyncState.Paused) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") + } else if (!canReachServer) { + Timber.v("No network. Waiting...") + updateStateTo(SyncState.NoNetwork) + // We force retrying in RETRY_WAIT_TIME_MS maximum. Otherwise it will be unlocked by onConnectivityChanged() or restart() + retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { + synchronized(lock) { + canReachServer = true + lock.notify() + } + } + synchronized(lock) { lock.wait() } + Timber.v("...retry") + } else if (!isTokenValid) { + Timber.v("Token is invalid. Waiting...") + updateStateTo(SyncState.InvalidToken) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") + } else { + if (state !is SyncState.Running) { + updateStateTo(SyncState.Running(afterPause = true)) + } + // No timeout after a pause + val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } + Timber.v("Execute sync request with timeout $timeout") + val params = SyncTask.Params(timeout) + val sync = syncScope.launch { + doSync(params) + } + runBlocking { + sync.join() + } + Timber.v("...Continue") + } + } + Timber.v("Sync killed") + updateStateTo(SyncState.Killed) + backgroundDetectionObserver.unregister(this) + networkConnectivityChecker.unregister(this) + } + + private suspend fun doSync(params: SyncTask.Params) { + try { + syncTask.execute(params) + } catch (failure: Throwable) { + if (failure is Failure.NetworkConnection) { + canReachServer = false + } + if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { + // Timeout are not critical + Timber.v("Timeout") + } else if (failure is Failure.Cancelled) { + Timber.v("Cancelled") + } else if (failure.isTokenError()) { + // No token or invalid token, stop the thread + Timber.w(failure, "Token error") + isStarted = false + isTokenValid = false + } else { + Timber.e(failure) + if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { + // Wait 10s before retrying + Timber.v("Wait 10s") + delay(RETRY_WAIT_TIME_MS) + } + } + } finally { + state.let { + if (it is SyncState.Running && it.afterPause) { + updateStateTo(SyncState.Running(afterPause = false)) + } + } + } + } + + private fun updateStateTo(newState: SyncState) { + Timber.v("Update state from $state to $newState") + if (newState == state) { + return + } + state = newState + debouncer.debounce("post_state", Runnable { + liveState.value = newState + }, 150) + } + + override fun onMoveToForeground() { + restart() + } + + override fun onMoveToBackground() { + pause() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..e702de3573caffc07afa359a2811798793b382b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.job + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val DEFAULT_LONG_POOL_TIMEOUT = 0L + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class SyncWorker(context: Context, + workerParameters: WorkerParameters +) : CoroutineWorker(context, workerParameters) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, + val automaticallyRetry: Boolean = false, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var syncTask: SyncTask + @Inject lateinit var taskExecutor: TaskExecutor + @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker + + override suspend fun doWork(): Result { + Timber.i("Sync work starting") + val params = WorkerParamsFactory.fromData<Params>(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + return runCatching { + doSync(params.timeout) + }.fold( + { Result.success() }, + { failure -> + if (failure.isTokenError() || !params.automaticallyRetry) { + Result.failure() + } else { + Result.retry() + } + } + ) + } + + private suspend fun doSync(timeout: Long) { + val taskParams = SyncTask.Params(timeout) + syncTask.execute(taskParams) + } + + companion object { + private const val BG_SYNC_WORK_NAME = "BG_SYNCP" + + fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) + .setInputData(data) + .build() + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + } + + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) + .build() + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + } + + fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { + workManagerProvider.workManager + .cancelUniqueWork(BG_SYNC_WORK_NAME) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..226486596ed620f6ae86ac590e18abc35eb95123 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the device information + */ +@JsonClass(generateAdapter = true) +internal data class DeviceInfo( + /** + * The owner user id + */ + val user_id: String? = null, + + /** + * The device id + */ + val device_id: String? = null, + + /** + * The device display name + */ + val display_name: String? = null, + + /** + * The last time this device has been seen. + */ + val last_seen_ts: Long = 0, + + /** + * The last ip address + */ + val last_seen_ip: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..df7906019275173b4d5248d78f8ed2f5bc260d44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the device list response from a sync request + */ +@JsonClass(generateAdapter = true) +internal data class DeviceListResponse( + // user ids list which have new crypto devices + val changed: List<String> = emptyList(), + // List of user ids who are no more tracked. + val left: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..36fe4acfa1a795af5ef40ca4b1a0ac427c5b093f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class DeviceOneTimeKeysCountSyncResponse( + @Json(name = "signed_curve25519") val signedCurve25519: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..ec59e8f2c8de43d9ca58d06b5065f16940a2e80d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the + */ +@JsonClass(generateAdapter = true) +internal data class DevicesListResponse( + val devices: List<DeviceInfo>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a7ed53cae7e4cb01a3335ae49a12fbe4f6cbc7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupSyncProfile( + /** + * The name of the group, if any. May be nil. + */ + @Json(name = "name") val name: String? = null, + + /** + * The URL for the group's avatar. May be nil. + */ + @Json(name = "avatar_url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..68557d1d9abe8f08e77670c66dd40208b72b134b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupsSyncResponse( + /** + * Joined groups: An array of groups ids. + */ + @Json(name = "join") val join: Map<String, Any> = emptyMap(), + + /** + * Invitations. The groups that the user has been invited to: keys are groups ids. + */ + @Json(name = "invite") val invite: Map<String, InvitedGroupSync> = emptyMap(), + + /** + * Left groups. An array of groups ids: the groups that the user has left or been banned from. + */ + @Json(name = "leave") val leave: Map<String, Any> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt new file mode 100644 index 0000000000000000000000000000000000000000..cae6bc36a4c1da729eb0e43cb766c2c4f5574ddd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class InvitedGroupSync( + /** + * The identifier of the inviter. + */ + @Json(name = "inviter") val inviter: String? = null, + + /** + * The group profile. + */ + @Json(name = "profile") val profile: GroupSyncProfile? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt new file mode 100644 index 0000000000000000000000000000000000000000..efd1b50b3bf04c5a1404e71ee0d5d9402e63e934 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// InvitedRoomSync represents a room invitation during server sync v2. +@JsonClass(generateAdapter = true) +internal data class InvitedRoomSync( + + /** + * The state of a room that the user has been invited to. These state events may only have the 'sender', 'type', 'state_key' + * and 'content' keys present. These events do not replace any state that the client already has for the room, for example if + * the client has archived the room. Instead the client should keep two separate copies of the state: the one from the 'invite_state' + * and one from the archived 'state'. If the client joins the room then the current state will be given as a delta against the + * archived 'state' not the 'invite_state'. + */ + @Json(name = "invite_state") val inviteState: RoomInviteState? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c6057d152e7d3e6bbe79f6ccf623c5e7f7d628f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// PresenceSyncResponse represents the updates to the presence status of other users during server sync v2. +@JsonClass(generateAdapter = true) +internal data class PresenceSyncResponse( + + /** + * List of presence events (array of Event with type m.presence). + */ + val events: List<Event>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt new file mode 100644 index 0000000000000000000000000000000000000000..e37d4f58c7bd84e459cb7e4f2ff0c29c4fb23a74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomInviteState represents the state of a room that the user has been invited to. +@JsonClass(generateAdapter = true) +internal data class RoomInviteState( + + /** + * List of state events (array of MXEvent). + */ + @Json(name = "events") val events: List<Event> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..df53eabd808ab5acb8697e93cb1818c5214e2bdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Class representing a room from a JSON response from room or global initial sync. + */ +@JsonClass(generateAdapter = true) +internal data class RoomResponse( + // The room identifier. + val roomId: String? = null, + + // The last recent messages of the room. + val messages: TokensChunkResponse<Event>? = null, + + // The state events. + val state: List<Event>? = null, + + // The private data that this user has attached to this room. + val accountData: List<Event>? = null, + + // The current user membership in this room. + val membership: String? = null, + + // The room visibility (public/private). + val visibility: String? = null, + + // The matrix id of the inviter in case of pending invitation. + val inviter: String? = null, + + // The invite event if membership is invite. + val invite: Event? = null, + + // The presence status of other users + // (Provided in case of room initial sync @see http://matrix.org/docs/api/client-server/#!/-rooms/get_room_sync_data)). + val presence: List<Event>? = null, + + // The read receipts (Provided in case of room initial sync). + val receipts: List<Event>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt new file mode 100644 index 0000000000000000000000000000000000000000..08556e800d7c089823ba0e58f7baf5a425019410 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// RoomSync represents the response for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSync( + /** + * The state updates for the room. + */ + @Json(name = "state") val state: RoomSyncState? = null, + + /** + * The timeline of messages and state changes in the room. + */ + @Json(name = "timeline") val timeline: RoomSyncTimeline? = null, + + /** + * The ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing, receipts). + */ + @Json(name = "ephemeral") val ephemeral: RoomSyncEphemeral? = null, + + /** + * The account data events for the room (e.g. tags). + */ + @Json(name = "account_data") val accountData: RoomSyncAccountData? = null, + + /** + * The notification counts for the room. + */ + @Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null, + + /** + * The room summary + */ + @Json(name = "summary") val summary: RoomSyncSummary? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt new file mode 100644 index 0000000000000000000000000000000000000000..13ea47a5052e354f62202ec1342abc15edc4bd68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RoomSyncAccountData( + /** + * List of account data events (array of Event). + */ + @Json(name = "events") val events: List<Event> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d0e9e825f55e1d381102fb5f75b4dae079c52f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncEphemeral represents the ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing). +@JsonClass(generateAdapter = true) +internal data class RoomSyncEphemeral( + /** + * List of ephemeral events (array of Event). + */ + @Json(name = "events") val events: List<Event> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt new file mode 100644 index 0000000000000000000000000000000000000000..f30e5a082d85808fa3d1bc1498b19ad349a9100e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncState represents the state updates for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSyncState( + + /** + * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. + */ + @Json(name = "events") val events: List<Event> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt new file mode 100644 index 0000000000000000000000000000000000000000..a2dddb9e8c9dfb7a61871da10f248335d4656f95 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomSyncSummary( + + /** + * Present only if the room has no m.room.name or m.room.canonical_alias. + * + * + * Lists the mxids of the first 5 members in the room who are currently joined or invited (ordered by stream ordering as seen on the server, + * to avoid it jumping around if/when topological order changes). As the heroes’ membership status changes, the list changes appropriately + * (sending the whole new list in the next /sync response). This list always excludes the current logged in user. If there are no joined or + * invited users, it lists the parted and banned ones instead. Servers can choose to send more or less than 5 members if they must, but 5 + * seems like a good enough number for most naming purposes. Clients should use all the provided members to name the room, but may truncate + * the list if helpful for UX + */ + @Json(name = "m.heroes") val heroes: List<String> = emptyList(), + + /** + * The number of m.room.members in state 'joined' (including the syncing user) (can be null) + */ + @Json(name = "m.joined_member_count") val joinedMembersCount: Int? = null, + + /** + * The number of m.room.members in state 'invited' (can be null) + */ + @Json(name = "m.invited_member_count") val invitedMembersCount: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt new file mode 100644 index 0000000000000000000000000000000000000000..29e5d2089bb95a6522138e56d1f058b025808158 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncTimeline represents the timeline of messages and state changes for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSyncTimeline( + + /** + * List of events (array of Event). + */ + @Json(name = "events") val events: List<Event> = emptyList(), + + /** + * Boolean which tells whether there are more events on the server + */ + @Json(name = "limited") val limited: Boolean = false, + + /** + * If the batch was limited then this is a token that can be supplied to the server to retrieve more events + */ + @Json(name = "prev_batch") val prevToken: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt new file mode 100644 index 0000000000000000000000000000000000000000..bbcec474e25683461957b4e59756a6b442d4f72b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * `MXRoomSyncUnreadNotifications` represents the unread counts for a room. + */ +@JsonClass(generateAdapter = true) +internal data class RoomSyncUnreadNotifications( + /** + * List of account data events (array of Event). + */ + @Json(name = "events") val events: List<Event>? = null, + + /** + * The number of unread messages that match the push notification rules. + */ + @Json(name = "notification_count") val notificationCount: Int? = null, + + /** + * The number of highlighted unread messages (subset of notifications). + */ + @Json(name = "highlight_count") val highlightCount: Int? = null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..79000edf4081bd6c4f09712b71287d3ea14d9e43 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// RoomsSyncResponse represents the rooms list in server sync v2 response. +@JsonClass(generateAdapter = true) +internal data class RoomsSyncResponse( + /** + * Joined rooms: keys are rooms ids. + */ + @Json(name = "join") val join: Map<String, RoomSync> = emptyMap(), + + /** + * Invitations. The rooms that the user has been invited to: keys are rooms ids. + */ + @Json(name = "invite") val invite: Map<String, InvitedRoomSync> = emptyMap(), + + /** + * Left rooms. The rooms that the user has left or been banned from: keys are rooms ids. + */ + @Json(name = "leave") val leave: Map<String, RoomSync> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..e57c6cd1f8f5fd6a95e823031a91218e3c8abbf8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync + +// SyncResponse represents the request response for server sync v2. +@JsonClass(generateAdapter = true) +internal data class SyncResponse( + /** + * The user private data. + */ + @Json(name = "account_data") val accountData: UserAccountDataSync? = null, + + /** + * The opaque token for the end. + */ + @Json(name = "next_batch") val nextBatch: String? = null, + + /** + * The updates to the presence status of other users. + */ + @Json(name = "presence") val presence: PresenceSyncResponse? = null, + + /* + * Data directly sent to one of user's devices. + */ + @Json(name = "to_device") val toDevice: ToDeviceSyncResponse? = null, + + /** + * List of rooms. + */ + @Json(name = "rooms") val rooms: RoomsSyncResponse? = null, + + /** + * Devices list update + */ + @Json(name = "device_lists") val deviceLists: DeviceListResponse? = null, + + /** + * One time keys management + */ + @Json(name = "device_one_time_keys_count") + val deviceOneTimeKeysCount: DeviceOneTimeKeysCountSyncResponse? = null, + + /** + * List of groups. + */ + @Json(name = "groups") val groups: GroupsSyncResponse? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bc9f0a3fa6af52f8377a07212efe4938a9bec96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// ToDeviceSyncResponse represents the data directly sent to one of user's devices. +@JsonClass(generateAdapter = true) +internal data class ToDeviceSyncResponse( + + /** + * List of direct-to-device events. + */ + val events: List<Event>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..813c300ec9415be63b0e05e6305fa66985a4b6b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TokensChunkResponse<T>( + val start: String? = null, + val end: String? = null, + val chunk: List<T>? = null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..57cd387243ef508da7767cd4286ca64da1a82242 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AcceptedTermsContent( + @Json(name = "accepted") val acceptedTerms: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..54aa5cb0b99f9c348656d1d35ce52ba5bc53ee66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BreadcrumbsContent( + @Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..fbaccf08c6091d8c1ed426b09ab612a42116f684 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +typealias DirectMessagesContent = Map<String, List<String>> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..5328525c533f510d4aedb4b1f6db7c623781de56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityServerContent( + @Json(name = "base_url") val baseUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..1095d2e76d4710ed8fb74cc376feba067b8fb798 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.emptyJsonDict + +@JsonClass(generateAdapter = true) +internal data class IgnoredUsersContent( + /** + * Required. The map of users to ignore. UserId -> empty object for future enhancement + */ + @Json(name = "ignored_users") val ignoredUsers: Map<String, Any> +) { + + companion object { + fun createWithUserIds(userIds: List<String>): IgnoredUsersContent { + return IgnoredUsersContent( + ignoredUsers = userIds.associateWith { emptyJsonDict } + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt new file mode 100644 index 0000000000000000000000000000000000000000..358f090bbd79e51141d030db48d342454c151be3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataSync( + @Json(name = "events") val list: List<UserAccountDataEvent> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000000000000000000000000000000000..497d30fdcad7d7f121fbf63e93615782a3f43eab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represent a list of urls of terms the user wants to accept + */ +@JsonClass(generateAdapter = true) +internal data class AcceptTermsBody( + @Json(name = "user_accepts") + val acceptedTermUrls: List<String> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000000000000000000000000000000000..f887754b6bd299305e2ee4bbb9f387e8fc17f182 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.terms + +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.terms.GetTermsResponse +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI +import org.matrix.android.sdk.internal.session.identity.IdentityRegisterTask +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal class DefaultTermsService @Inject constructor( + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + private val accountDataDataSource: AccountDataDataSource, + private val termsAPI: TermsAPI, + private val retrofitFactory: RetrofitFactory, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : TermsService { + override fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + callback: MatrixCallback<GetTermsResponse>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val url = buildUrl(baseUrl, serviceType) + val termsResponse = executeRequest<TermsResponse>(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List<String>, + token: String?, + callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val url = buildUrl(baseUrl, serviceType) + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + + executeRequest<Unit>(null) { + apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") + } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) + } + } + + private suspend fun getToken(url: String): String { + // TODO This is duplicated code see DefaultIdentityService + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + private fun buildUrl(baseUrl: String, serviceType: TermsService.ServiceType): String { + val servicePath = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> NetworkConstants.URI_INTEGRATION_MANAGER_PATH + TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 + } + return "${baseUrl.ensureTrailingSlash()}$servicePath" + } + + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set<String> { + return accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel<AcceptedTermsContent>() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..950c0a151b69bdddc05d0d71c4af11d75f334f0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.terms + +import org.matrix.android.sdk.internal.network.HttpHeaders +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url + +internal interface TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call<TermsResponse> + + /** + * This request requires authentication + */ + @POST + fun agreeToTerms(@Url url: String, + @Body params: AcceptTermsBody, + @Header(HttpHeaders.Authorization) token: String): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..7aa97cd1ccd8a2a86d8d09381b8569ce17465b03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.terms + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionScope +import okhttp3.OkHttpClient + +@Module +internal abstract class TermsModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesTermsAPI(@UnauthenticatedWithCertificate unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..240291c09f36e7149665f9b7b30033da9a1ec937 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@JsonClass(generateAdapter = true) +data class TermsResponse( + @Json(name = "policies") + val policies: JsonDict? = null +) { + + fun getLocalizedTerms(userLanguage: String, + defaultLanguage: String = "en"): List<LocalizedFlowDataLoginTerms> { + return policies?.map { + val tos = policies[it.key] as? Map<*, *> ?: return@map null + ((tos[userLanguage] ?: tos[defaultLanguage]) as? Map<*, *>)?.let { termsMap -> + val name = termsMap[NAME] as? String + val url = termsMap[URL] as? String + LocalizedFlowDataLoginTerms( + policyName = it.key, + localizedUrl = url, + localizedName = name, + version = tos[VERSION] as? String + ) + } + }?.filterNotNull().orEmpty() + } + + private companion object { + const val VERSION = "version" + const val NAME = "name" + const val URL = "url" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt new file mode 100644 index 0000000000000000000000000000000000000000..0fa557467cf72678135e8c89824e0b3143057d05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.typing + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTracker { + + private val typingUsers = mutableMapOf<String, List<SenderInfo>>() + + /** + * Set all currently typing users for a room (excluding yourself) + */ + fun setTypingUsersFromRoom(roomId: String, senderInfoList: List<SenderInfo>) { + val hasNewValue = typingUsers[roomId] != senderInfoList + if (hasNewValue) { + typingUsers[roomId] = senderInfoList + } + } + + override fun getTypingUsers(roomId: String): List<SenderInfo> { + return typingUsers[roomId] ?: emptyList() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt new file mode 100644 index 0000000000000000000000000000000000000000..e79893e752e8c3b124b4110c3c7be99906cf8bf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.model.SearchUserTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource, + private val searchUserTask: SearchUserTask, + private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, + private val taskExecutor: TaskExecutor) : UserService { + + override fun getUser(userId: String): User? { + return userDataSource.getUser(userId) + } + + override fun getUserLive(userId: String): LiveData<Optional<User>> { + return userDataSource.getUserLive(userId) + } + + override fun getUsersLive(): LiveData<List<User>> { + return userDataSource.getUsersLive() + } + + override fun getPagedUsersLive(filter: String?, excludedUserIds: Set<String>?): LiveData<PagedList<User>> { + return userDataSource.getPagedUsersLive(filter, excludedUserIds) + } + + override fun getIgnoredUsersLive(): LiveData<List<User>> { + return userDataSource.getIgnoredUsersLive() + } + + override fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set<String>, + callback: MatrixCallback<List<User>>): Cancelable { + val params = SearchUserTask.Params(limit, search, excludedUserIds) + return searchUserTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun ignoreUserIds(userIds: List<String>, callback: MatrixCallback<Unit>): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unIgnoreUserIds(userIds: List<String>, callback: MatrixCallback<Unit>): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToUnIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b9a5f4aa4416eb8b2501336f870cbad895cc080 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.user.model.SearchUsersParams +import org.matrix.android.sdk.internal.session.user.model.SearchUsersResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface SearchUserAPI { + + /** + * Perform a user search. + * + * @param searchUsersParams the search params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") + fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call<SearchUsersResponse> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..dd3c6856c0af7d0c041d9476cc182e21b311e00c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.model.UserEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.fetchCopied +import io.realm.Case +import javax.inject.Inject + +internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder<Int, User> by lazy { + LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) + } + + fun getUser(userId: String): User? { + val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } + ?: return null + + return userEntity.asDomain() + } + + fun getUserLive(userId: String): LiveData<Optional<User>> { + val liveData = monarchy.findAllMappedWithChanges( + { UserEntity.where(it, userId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getUsersLive(): LiveData<List<User>> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + }, + { it.asDomain() } + ) + } + + fun getPagedUsersLive(filter: String?, excludedUserIds: Set<String>?): LiveData<PagedList<User>> { + realmDataSourceFactory.updateQuery { realm -> + val query = realm.where(UserEntity::class.java) + if (filter.isNullOrEmpty()) { + query.isNotEmpty(UserEntityFields.USER_ID) + } else { + query + .beginGroup() + .contains(UserEntityFields.DISPLAY_NAME, filter, Case.INSENSITIVE) + .or() + .contains(UserEntityFields.USER_ID, filter) + .endGroup() + } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } + query.sort(UserEntityFields.DISPLAY_NAME) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + } + + fun getIgnoredUsersLive(): LiveData<List<User>> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(IgnoredUserEntity::class.java) + .isNotEmpty(IgnoredUserEntityFields.USER_ID) + .sort(IgnoredUserEntityFields.USER_ID) + }, + { getUser(it.userId) ?: User(userId = it.userId) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..6333a87a0b1014a20e9afcfe160a3c45b9c7319e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.UserEntity + +internal object UserEntityFactory { + + fun create(userId: String, roomMember: RoomMemberContent): UserEntity { + return UserEntity( + userId = userId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..51e4339061be5017c7d18814abf930169007a067 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSaveIgnoredUsersTask +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultUpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.accountdata.SaveIgnoredUsersTask +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.model.DefaultSearchUserTask +import org.matrix.android.sdk.internal.session.user.model.SearchUserTask +import retrofit2.Retrofit + +@Module +internal abstract class UserModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI { + return retrofit.create(SearchUserAPI::class.java) + } + } + + @Binds + abstract fun bindUserService(service: DefaultUserService): UserService + + @Binds + abstract fun bindSearchUserTask(task: DefaultSearchUserTask): SearchUserTask + + @Binds + abstract fun bindSaveIgnoredUsersTask(task: DefaultSaveIgnoredUsersTask): SaveIgnoredUsersTask + + @Binds + abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask + + @Binds + abstract fun bindUserStore(store: RealmUserStore): UserStore +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea64cb9a2c71e44678f23b1f9a56238cc77a8716 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface UserStore { + suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null) +} + +internal class RealmUserStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : UserStore { + + override suspend fun createOrUpdate(userId: String, displayName: String?, avatarUrl: String?) { + monarchy.awaitTransaction { + val userEntity = UserEntity(userId, displayName ?: "", avatarUrl ?: "") + it.insertOrUpdate(userEntity) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..25336cacf73e429aa07688c5635d5974b67511a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.PUT +import retrofit2.http.Path + +interface AccountDataAPI { + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the type + * @param params the put params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") + fun setAccountData(@Path("userId") userId: String, + @Path("type") type: String, + @Body params: Any): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..5384a1ba9cbf9c75544b01faa554beb4ba816043 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +/** + * Tag class to identify every account data content + */ +internal interface AccountDataContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..d54bfdd63d856ebfd7949259b3f70dbf2032e3ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..291d0bfaf751d37b57c7e37ca5fa95b088ec9efb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class AccountDataModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesAccountDataAPI(retrofit: Retrofit): AccountDataAPI { + return retrofit.create(AccountDataAPI::class.java) + } + } + + @Binds + abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask + + @Binds + abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask + + @Binds + abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt new file mode 100644 index 0000000000000000000000000000000000000000..2bf17058551fa49605c659011420f894da878421 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultAccountDataService @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val accountDataDataSource: AccountDataDataSource, + private val taskExecutor: TaskExecutor +) : AccountDataService { + + override fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return accountDataDataSource.getAccountDataEvent(type) + } + + override fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> { + return accountDataDataSource.getLiveAccountDataEvent(type) + } + + override fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> { + return accountDataDataSource.getAccountDataEvents(types) + } + + override fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> { + return accountDataDataSource.getLiveAccountDataEvents(types) + } + + override fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>?): Cancelable { + return updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( + type = type, + any = content + )) { + this.retryCount = 5 + this.callback = object : MatrixCallback<Unit> { + override fun onSuccess(data: Unit) { + // TODO Move that to the task (but it created a circular dependencies...) + monarchy.runTransactionSync { realm -> + userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) + } + callback?.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bec45a20366670ffedc797b7cce29b884418bd8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getDirectRooms +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class DirectChatsHelper @Inject constructor(@SessionDatabase + private val realmConfiguration: RealmConfiguration) { + + /** + * @return a map of userId <-> list of roomId + */ + fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> { + return Realm.getInstance(realmConfiguration).use { realm -> + RoomSummaryEntity.getDirectRooms(realm) + .asSequence() + .filter { it.roomId != filterRoomId && it.directUserId != null } + .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ef28954e0b92bd4767ab00b43f7ace8de086498 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.RealmList +import javax.inject.Inject + +/** + * Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally + */ +internal interface SaveBreadcrumbsTask : Task<SaveBreadcrumbsTask.Params, Unit> { + data class Params( + val recentRoomIds: List<String> + ) +} + +internal class DefaultSaveBreadcrumbsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : SaveBreadcrumbsTask { + + override suspend fun execute(params: SaveBreadcrumbsTask.Params) { + monarchy.awaitTransaction { realm -> + // Get or create a breadcrumbs entity + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList<String>().apply { addAll(params.recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + params.recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9141aabc80138e892e2e6df4379a91ae49e29c2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * Save the ignored users list in DB + */ +internal interface SaveIgnoredUsersTask : Task<SaveIgnoredUsersTask.Params, Unit> { + data class Params( + val userIds: List<String> + ) +} + +internal class DefaultSaveIgnoredUsersTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : SaveIgnoredUsersTask { + + override suspend fun execute(params: SaveIgnoredUsersTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current ignored users + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // And save the new received list + params.userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab602dd6039869d91cc7d6d09112014025cf63ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.fetchCopied +import javax.inject.Inject + +// Use the same arbitrary value than Riot-Web +private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20 + +internal interface UpdateBreadcrumbsTask : Task<UpdateBreadcrumbsTask.Params, Unit> { + data class Params( + val newTopRoomId: String + ) +} + +internal class DefaultUpdateBreadcrumbsTask @Inject constructor( + private val saveBreadcrumbsTask: SaveBreadcrumbsTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + @SessionDatabase private val monarchy: Monarchy +) : UpdateBreadcrumbsTask { + + override suspend fun execute(params: UpdateBreadcrumbsTask.Params) { + val newBreadcrumbs = + // Get the breadcrumbs entity, if any + monarchy.fetchCopied { BreadcrumbsEntity.get(it) } + ?.recentRoomIds + ?.apply { + // Modify the list to add the newTopRoomId first + // Ensure the newTopRoomId is not already in the list + remove(params.newTopRoomId) + // Add the newTopRoomId at first position + add(0, params.newTopRoomId) + } + ?.take(MAX_BREADCRUMBS_ROOMS_NUMBER) + ?: listOf(params.newTopRoomId) + + // Update the DB locally, do not wait for the sync + saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs)) + + // FIXME It can remove the previous breadcrumbs, if not synced yet + // And update account data + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams( + breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs) + )) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b47a25187bd7c7932fefba2ba0b8087abd9a014d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface UpdateIgnoredUserIdsTask : Task<UpdateIgnoredUserIdsTask.Params, Unit> { + + data class Params( + val userIdsToIgnore: List<String> = emptyList(), + val userIdsToUnIgnore: List<String> = emptyList() + ) +} + +internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @SessionDatabase private val monarchy: Monarchy, + private val saveIgnoredUsersTask: SaveIgnoredUsersTask, + @UserId private val userId: String, + private val eventBus: EventBus +) : UpdateIgnoredUserIdsTask { + + override suspend fun execute(params: UpdateIgnoredUserIdsTask.Params) { + // Get current list + val ignoredUserIds = monarchy.fetchAllMappedSync( + { realm -> realm.where(IgnoredUserEntity::class.java) }, + { it.userId } + ).toMutableSet() + + val original = ignoredUserIds.toSet() + + ignoredUserIds.removeAll { it in params.userIdsToUnIgnore } + ignoredUserIds.addAll(params.userIdsToIgnore) + + if (original == ignoredUserIds) { + // No change + return + } + + val list = ignoredUserIds.toList() + val body = IgnoredUsersContent.createWithUserIds(list) + + executeRequest<Unit>(eventBus) { + apiCall = accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body) + } + + // Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update) + saveIgnoredUsersTask.execute(SaveIgnoredUsersTask.Params(list)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..a68d76f25b4b208f1e816c20da45ded432c3c505 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.integrationmanager.AllowedWidgetsContent +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationProvisioningContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Params, Unit> { + + interface Params { + val type: String + fun getData(): Any + } + + data class IdentityParams(override val type: String = UserAccountDataTypes.TYPE_IDENTITY_SERVER, + private val identityContent: IdentityServerContent + ) : Params { + + override fun getData(): Any { + return identityContent + } + } + + data class AcceptedTermsParams(override val type: String = UserAccountDataTypes.TYPE_ACCEPTED_TERMS, + private val acceptedTermsContent: AcceptedTermsContent + ) : Params { + + override fun getData(): Any { + return acceptedTermsContent + } + } + + // TODO Use [UserAccountDataDirectMessages] class? + data class DirectChatParams(override val type: String = UserAccountDataTypes.TYPE_DIRECT_MESSAGES, + private val directMessages: Map<String, List<String>> + ) : Params { + + override fun getData(): Any { + return directMessages + } + } + + data class BreadcrumbsParams(override val type: String = UserAccountDataTypes.TYPE_BREADCRUMBS, + private val breadcrumbsContent: BreadcrumbsContent + ) : Params { + + override fun getData(): Any { + return breadcrumbsContent + } + } + + data class AllowedWidgets(override val type: String = UserAccountDataTypes.TYPE_ALLOWED_WIDGETS, + private val allowedWidgetsContent: AllowedWidgetsContent) : Params { + + override fun getData(): Any { + return allowedWidgetsContent + } + } + + data class IntegrationProvisioning(override val type: String = UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING, + private val integrationProvisioningContent: IntegrationProvisioningContent) : Params { + + override fun getData(): Any { + return integrationProvisioningContent + } + } + + data class AnyParams(override val type: String, + private val any: Any + ) : Params { + override fun getData(): Any { + return any + } + } +} + +internal class DefaultUpdateUserAccountDataTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : UpdateUserAccountDataTask { + + override suspend fun execute(params: UpdateUserAccountDataTask.Params) { + return executeRequest(eventBus) { + apiCall = accountDataApi.setAccountData(userId, params.type, params.getData()) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt new file mode 100644 index 0000000000000000000000000000000000000000..4299794c161502d22082b29227b58e11c76450ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchUser( + @Json(name = "user_id") val userId: String, + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f587d7f8d76f17d4f23a17d4b3245f69fc33649 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.user.SearchUserAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SearchUserTask : Task<SearchUserTask.Params, List<User>> { + + data class Params( + val limit: Int, + val search: String, + val excludedUserIds: Set<String> + ) +} + +internal class DefaultSearchUserTask @Inject constructor( + private val searchUserAPI: SearchUserAPI, + private val eventBus: EventBus +) : SearchUserTask { + + override suspend fun execute(params: SearchUserTask.Params): List<User> { + val response = executeRequest<SearchUsersResponse>(eventBus) { + apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + } + return response.users.map { + User(it.userId, it.displayName, it.avatarUrl) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8e855a004e4a88e50519d91daa26db7b02a2478 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an user search parameters + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersParams( + // the searched term + @Json(name = "search_term") val searchTerm: String, + // set a limit to the request response + @Json(name = "limit") val limit: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..646d8e63bfbc85fd42cfa985029a532f8667e0bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an users search response + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersResponse( + @Json(name = "limited") val limited: Boolean = false, + @Json(name = "results") val users: List<SearchUser> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..9f7981a95bfe18270cbe5dce89977667cdd76a8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.database.query.whereStateKey +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface CreateWidgetTask : Task<CreateWidgetTask.Params, Unit> { + + data class Params( + val roomId: String, + val widgetId: String, + val content: Content + ) +} + +internal class DefaultCreateWidgetTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus) : CreateWidgetTask { + + override suspend fun execute(params: CreateWidgetTask.Params) { + executeRequest<Unit>(eventBus) { + apiCall = roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = EventType.STATE_ROOM_WIDGET_LEGACY, + stateKey = params.widgetId, + params = params.content + ) + } + awaitNotEmptyResult(monarchy.realmConfiguration, 30_000L) { + CurrentStateEventEntity + .whereStateKey(it, params.roomId, type = EventType.STATE_ROOM_WIDGET_LEGACY, stateKey = params.widgetId) + .and() + .equalTo(CurrentStateEventEntityFields.ROOT.SENDER, userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f8f2c1f94a0b4a34f5a274294c030edfd75b6ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import android.os.Build +import android.webkit.JavascriptInterface +import android.webkit.WebView +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.createUIHandler +import timber.log.Timber +import java.lang.reflect.Type +import java.util.HashMap +import javax.inject.Inject + +internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi, + private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) + : WidgetPostAPIMediator { + + private val jsonAdapter = moshi.adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) + + private var handler: WidgetPostAPIMediator.Handler? = null + private var webView: WebView? = null + + private val uiHandler = createUIHandler() + + override fun setWebView(webView: WebView) { + this.webView = webView + webView.addJavascriptInterface(this, "Android") + } + + override fun clearWebView() { + webView?.removeJavascriptInterface("Android") + webView = null + } + + override fun setHandler(handler: WidgetPostAPIMediator.Handler?) { + this.handler = handler + } + + override fun injectAPI() { + val js = widgetPostMessageAPIProvider.get() + if (js != null) { + uiHandler.post { + webView?.loadUrl("javascript:$js") + } + } + } + + @JavascriptInterface + fun onWidgetEvent(jsonEventData: String) { + Timber.d("BRIDGE onWidgetEvent : $jsonEventData") + try { + val dataAsDict = jsonAdapter.fromJson(jsonEventData) + @Suppress("UNCHECKED_CAST") + val eventData = (dataAsDict?.get("event.data") as? JsonDict) ?: return + onWidgetMessage(eventData) + } catch (e: Exception) { + Timber.e(e, "## onWidgetEvent() failed") + } + } + + private fun onWidgetMessage(eventData: JsonDict) { + try { + if (handler?.handleWidgetRequest(this, eventData) == false) { + sendError("", eventData) + } + } catch (e: Exception) { + Timber.e(e, "## onWidgetMessage() : failed") + sendError("", eventData) + } + } + + /* + * ********************************************************************************************* + * Message sending methods + * ********************************************************************************************* + */ + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendBoolResponse(response: Boolean, eventData: JsonDict) { + val jsString = if (response) "true" else "false" + sendResponse(jsString, eventData) + } + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendIntegerResponse(response: Int, eventData: JsonDict) { + sendResponse(response.toString() + "", eventData) + } + + /** + * Send an object response + * + * @param response the response + * @param eventData the modular data + */ + override fun <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict) { + var jsString: String? = null + if (response != null) { + val objectAdapter = moshi.adapter<T>(type) + try { + jsString = "JSON.parse('${objectAdapter.toJson(response)}')" + } catch (e: Exception) { + Timber.e(e, "## sendObjectResponse() : toJson failed ") + } + } + sendResponse(jsString ?: "null", eventData) + } + + /** + * Send success + * + * @param eventData the modular data + */ + override fun sendSuccess(eventData: JsonDict) { + val successResponse = mapOf("success" to true) + sendObjectResponse(Map::class.java, successResponse, eventData) + } + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + override fun sendError(message: String, eventData: JsonDict) { + Timber.e("## sendError() : eventData $eventData failed $message") + + // TODO: JS has an additional optional parameter: nestedError + val params = HashMap<String, Map<String, String>>() + val subMap = HashMap<String, String>() + subMap["message"] = message + params["error"] = subMap + sendObjectResponse(Map::class.java, params, eventData) + } + + /** + * Send the response to the javascript + * + * @param jsString the response data + * @param eventData the modular data + */ + private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post { + try { + val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");" + Timber.v("BRIDGE sendResponse: $functionLine") + // call the javascript method + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + webView?.loadUrl("javascript:$functionLine") + } else { + webView?.evaluateJavascript(functionLine, null) + } + } catch (e: Exception) { + Timber.e(e, "## sendResponse() failed ") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt new file mode 100644 index 0000000000000000000000000000000000000000..049b368fe5dfc0d48f0fcb1391aecb8be17bfed3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.Cancelable +import javax.inject.Inject +import javax.inject.Provider + +internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, + private val widgetURLFormatter: WidgetURLFormatter, + private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>) + : WidgetService { + + override fun getWidgetURLFormatter(): WidgetURLFormatter { + return widgetURLFormatter + } + + override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { + return widgetPostAPIMediator.get() + } + + override fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set<String>?, + excludedTypes: Set<String>? + ): List<Widget> { + return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set<String>?, + excludedTypes: Set<String>? + ): LiveData<List<Widget>> { + return widgetManager.getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getUserWidgetsLive( + widgetTypes: Set<String>?, + excludedTypes: Set<String>? + ): LiveData<List<Widget>> { + return widgetManager.getUserWidgetsLive(widgetTypes, excludedTypes) + } + + override fun getUserWidgets( + widgetTypes: Set<String>?, + excludedTypes: Set<String>? + ): List<Widget> { + return widgetManager.getUserWidgets(widgetTypes, excludedTypes) + } + + override fun createRoomWidget( + roomId: String, + widgetId: String, + content: Content, + callback: MatrixCallback<Widget> + ): Cancelable { + return widgetManager.createRoomWidget(roomId, widgetId, content, callback) + } + + override fun destroyRoomWidget( + roomId: String, + widgetId: String, + callback: MatrixCallback<Unit> + ): Cancelable { + return widgetManager.destroyRoomWidget(roomId, widgetId, callback) + } + + override fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + return widgetManager.hasPermissionsToHandleWidgets(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt new file mode 100644 index 0000000000000000000000000000000000000000..28bcf0021c71ed3d02a8b5518b93970de9bdd3a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask +import java.net.URLEncoder +import javax.inject.Inject + +@SessionScope +internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager, + private val getScalarTokenTask: GetScalarTokenTask, + private val matrixConfiguration: MatrixConfiguration +) : IntegrationManagerService.Listener, WidgetURLFormatter, SessionLifecycleObserver { + + private lateinit var currentConfig: IntegrationManagerConfig + private var whiteListedUrls: List<String> = emptyList() + + override fun onStart() { + setupWithConfiguration() + integrationManager.addListener(this) + } + + override fun onStop() { + integrationManager.removeListener(this) + } + + override fun onConfigurationChanged(configs: List<IntegrationManagerConfig>) { + setupWithConfiguration() + } + + private fun setupWithConfiguration() { + val preferredConfig = integrationManager.getPreferredConfig() + if (!this::currentConfig.isInitialized || preferredConfig != currentConfig) { + currentConfig = preferredConfig + whiteListedUrls = if (matrixConfiguration.integrationWidgetUrls.isEmpty()) { + listOf(preferredConfig.restUrl) + } else { + matrixConfiguration.integrationWidgetUrls + } + } + } + + /** + * Takes care of fetching a scalar token if required and build the final url. + */ + override suspend fun format(baseUrl: String, params: Map<String, String>, forceFetchScalarToken: Boolean, bypassWhitelist: Boolean): String { + return if (bypassWhitelist || isWhiteListed(baseUrl)) { + val taskParams = GetScalarTokenTask.Params(currentConfig.restUrl, forceFetchScalarToken) + val scalarToken = getScalarTokenTask.execute(taskParams) + buildString { + append(baseUrl) + appendParamToUrl("scalar_token", scalarToken) + appendParamsToUrl(params) + } + } else { + buildString { + append(baseUrl) + appendParamsToUrl(params) + } + } + } + + private fun isWhiteListed(url: String): Boolean { + val allowed: List<String> = whiteListedUrls + for (allowedUrl in allowed) { + if (url.startsWith(allowedUrl)) { + return true + } + } + return false + } + + private fun StringBuilder.appendParamsToUrl(params: Map<String, String>): StringBuilder { + params.forEach { (param, value) -> + appendParamToUrl(param, value) + } + return this + } + + private fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1d08ab295a72befce363ad2a26c42ccb9626c81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RegisterWidgetResponse( + @Json(name = "scalar_token") val scalarToken: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb9059b0897ac2cdb3eca246666ac67b535765c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory +import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import java.util.HashMap +import javax.inject.Inject + +@SessionScope +internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager, + private val accountDataDataSource: AccountDataDataSource, + private val stateEventDataSource: StateEventDataSource, + private val taskExecutor: TaskExecutor, + private val createWidgetTask: CreateWidgetTask, + private val widgetFactory: WidgetFactory, + @UserId private val userId: String) + + : IntegrationManagerService.Listener, SessionLifecycleObserver { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + integrationManager.addListener(this) + } + + override fun onStop() { + integrationManager.removeListener(this) + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): LiveData<List<Widget>> { + // Get all im.vector.modular.widgets state events in the room + val liveWidgetEvents = stateEventDataSource.getStateEventsLive( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return Transformations.map(liveWidgetEvents) { widgetEvents -> + widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + } + + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): List<Widget> { + // Get all im.vector.modular.widgets state events in the room + val widgetEvents: List<Event> = stateEventDataSource.getStateEvents( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + + private fun List<Event>.mapEventsToWidgets(widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null): List<Widget> { + val widgetEvents = this + // Widget id -> widget + val widgets: MutableMap<String, Widget> = HashMap() + // Order widgetEvents with the last event first + // There can be several im.vector.modular.widgets state events for a same widget but + // only the last one must be considered. + val sortedWidgetEvents = widgetEvents.sortedByDescending { + it.originServerTs + } + // Create each widget from its latest im.vector.modular.widgets state event + for (widgetEvent in sortedWidgetEvents) { // Filter widget types if required + val widget = widgetFactory.create(widgetEvent) ?: continue + val widgetType = widget.widgetContent.type ?: continue + if (widgetTypes != null && !widgetTypes.contains(widgetType)) { + continue + } + if (excludedTypes != null && excludedTypes.contains(widgetType)) { + continue + } + if (!widgets.containsKey(widget.widgetId)) { + widgets[widget.widgetId] = widget + } + } + return widgets.values.toList() + } + + fun getUserWidgetsLive( + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): LiveData<List<Widget>> { + val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) + return Transformations.map(widgetsAccountData) { + it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList() + } + } + + fun getUserWidgets( + widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null + ): List<Widget> { + val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) ?: return emptyList() + return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes) + } + + private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set<String>? = null, + excludedTypes: Set<String>? = null): List<Widget> { + return extractWidgetSequence(widgetFactory) + .filter { + val widgetType = it.widgetContent.type ?: return@filter false + (widgetTypes == null || widgetTypes.contains(widgetType)) + && (excludedTypes == null || !excludedTypes.contains(widgetType)) + } + .toList() + } + + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = content + ) + createWidgetTask.execute(params) + try { + getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first() + } catch (failure: Throwable) { + throw WidgetManagementFailure.CreationFailed + } + } + } + + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = emptyMap() + ) + createWidgetTask.execute(params) + } + } + + fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false + return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..56b2d947017cf0899be3aef7e2edb99ecfa049f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.internal.session.widgets.token.DefaultGetScalarTokenTask +import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask +import retrofit2.Retrofit + +@Module +internal abstract class WidgetModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesWidgetsAPI(retrofit: Retrofit): WidgetsAPI { + return retrofit.create(WidgetsAPI::class.java) + } + } + + @Binds + abstract fun bindWidgetService(service: DefaultWidgetService): WidgetService + + @Binds + abstract fun bindWidgetURLBuilder(formatter: DefaultWidgetURLFormatter): WidgetURLFormatter + + @Binds + abstract fun bindWidgetPostAPIMediator(mediator: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator + + @Binds + abstract fun bindCreateWidgetTask(task: DefaultCreateWidgetTask): CreateWidgetTask + + @Binds + abstract fun bindGetScalarTokenTask(task: DefaultGetScalarTokenTask): GetScalarTokenTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..5b26415ce5477b7b72b9e1d1650faec19a6a1dda --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import android.content.Context +import timber.log.Timber +import javax.inject.Inject + +internal class WidgetPostMessageAPIProvider @Inject constructor(private val context: Context) { + + private var postMessageAPIString: String? = null + + fun get(): String? { + if (postMessageAPIString == null) { + postMessageAPIString = readFromAsset(context) + } + return postMessageAPIString + } + + private fun readFromAsset(context: Context): String? { + return try { + context.assets.open("postMessageAPI.js").bufferedReader().use { + it.readText() + } + } catch (failure: Throwable) { + Timber.e(failure, "Reading postMessageAPI.js asset failed") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..bfcd27b28a2927e50367434fe9139843097e9dd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.widgets + +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +internal interface WidgetsAPI { + + /** + * register to the server + * + * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) + */ + @POST("register") + fun register(@Body body: RequestOpenIdTokenResponse, + @Query("v") version: String?): Call<RegisterWidgetResponse> + + @GET("account") + fun validateToken(@Query("scalar_token") scalarToken: String?, + @Query("v") version: String?): Call<Unit> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..cbbc11bb93c2bf5fe76629fca27c761a2169d351 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import dagger.Lazy +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionScope +import okhttp3.OkHttpClient +import javax.inject.Inject + +@SessionScope +internal class WidgetsAPIProvider @Inject constructor(@Unauthenticated private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory) { + + // Map to keep one WidgetAPI instance by serverUrl + private val widgetsAPIs = mutableMapOf<String, WidgetsAPI>() + + fun get(serverUrl: String): WidgetsAPI { + return widgetsAPIs.getOrPut(serverUrl) { + retrofitFactory.create(okHttpClient, serverUrl).create(WidgetsAPI::class.java) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt new file mode 100644 index 0000000000000000000000000000000000000000..f6dafd055337d269af1418d38ca927bf9ce2c459 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets.helper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.widgets.model.Widget + +internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence<Widget> { + return content.asSequence() + .mapNotNull { + @Suppress("UNCHECKED_CAST") + (it.value as? JsonDict)?.toModel<Event>() + }.mapNotNull { event -> + widgetFactory.create(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..2cbc9b23dc63307c03e27957b23028dc558df134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets.helper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.user.UserDataSource +import io.realm.Realm +import io.realm.RealmConfiguration +import java.net.URLEncoder +import javax.inject.Inject + +internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val userDataSource: UserDataSource, + @UserId private val userId: String) { + + fun create(widgetEvent: Event): Widget? { + val widgetContent = widgetEvent.content.toModel<WidgetContent>() + if (widgetContent?.url == null) return null + val widgetId = widgetEvent.stateKey ?: return null + val type = widgetContent.type ?: return null + val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) { + null + } else { + Realm.getInstance(realmConfiguration).use { + val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) + SenderInfo( + userId = widgetEvent.senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + } + val isAddedByMe = widgetEvent.senderId == userId + val computedUrl = widgetContent.computeURL(widgetEvent.roomId) + return Widget( + widgetContent = widgetContent, + event = widgetEvent, + widgetId = widgetId, + senderInfo = senderInfo, + isAddedByMe = isAddedByMe, + computedUrl = computedUrl, + type = WidgetType.fromString(type) + ) + } + + private fun WidgetContent.computeURL(roomId: String?): String? { + var computedUrl = url ?: return null + val myUser = userDataSource.getUser(userId) + computedUrl = computedUrl + .replace("\$matrix_user_id", userId) + .replace("\$matrix_display_name", myUser?.displayName ?: userId) + .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") + + if (roomId != null) { + computedUrl = computedUrl.replace("\$matrix_room_id", roomId) + } + for ((key, value) in data) { + if (value is String) { + computedUrl = computedUrl.replace("$$key", URLEncoder.encode(value, "utf-8")) + } + } + return computedUrl + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..58b0c610605b1fcb3982c7ec0bd5df763a3e9558 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets.token + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.widgets.RegisterWidgetResponse +import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure +import org.matrix.android.sdk.internal.session.widgets.WidgetsAPI +import org.matrix.android.sdk.internal.session.widgets.WidgetsAPIProvider +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetScalarTokenTask : Task<GetScalarTokenTask.Params, String> { + + data class Params( + val serverUrl: String, + val forceRefresh: Boolean = false + ) +} + +private const val WIDGET_API_VERSION = "1.1" + +internal class DefaultGetScalarTokenTask @Inject constructor(private val widgetsAPIProvider: WidgetsAPIProvider, + private val scalarTokenStore: ScalarTokenStore, + private val getOpenIdTokenTask: GetOpenIdTokenTask) : GetScalarTokenTask { + + override suspend fun execute(params: GetScalarTokenTask.Params): String { + val widgetsAPI = widgetsAPIProvider.get(params.serverUrl) + return if (params.forceRefresh) { + scalarTokenStore.clearToken(params.serverUrl) + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + val scalarToken = scalarTokenStore.getToken(params.serverUrl) + if (scalarToken == null) { + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + validateToken(widgetsAPI, params.serverUrl, scalarToken) + } + } + } + + private suspend fun getNewScalarToken(widgetsAPI: WidgetsAPI, serverUrl: String): String { + val openId = getOpenIdTokenTask.execute(Unit) + val registerWidgetResponse = executeRequest<RegisterWidgetResponse>(null) { + apiCall = widgetsAPI.register(openId, WIDGET_API_VERSION) + } + if (registerWidgetResponse.scalarToken == null) { + // Should not happen + throw IllegalStateException("Scalar token is null") + } + scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken) + return validateToken(widgetsAPI, serverUrl, registerWidgetResponse.scalarToken) + } + + private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { + return try { + executeRequest<Unit>(null) { + apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) + } + scalarToken + } catch (failure: Throwable) { + if (failure is Failure.ServerError && failure.httpCode == HttpsURLConnection.HTTP_FORBIDDEN) { + if (failure.error.code == MatrixError.M_TERMS_NOT_SIGNED) { + throw WidgetManagementFailure.TermsNotSignedException(serverUrl, scalarToken) + } else { + scalarTokenStore.clearToken(serverUrl) + getNewScalarToken(widgetsAPI, serverUrl) + } + } else { + throw failure + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6f8b3c8900dad04fa5c34c686ede9f1544b56f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.widgets.token + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.fetchCopyMap +import javax.inject.Inject + +internal class ScalarTokenStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getToken(apiUrl: String): String? { + return monarchy.fetchCopyMap({ realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst() + }, { scalarToken, _ -> + scalarToken.token + }) + } + + suspend fun setToken(apiUrl: String, token: String) { + monarchy.awaitTransaction { realm -> + val scalarTokenEntity = ScalarTokenEntity(apiUrl, token) + realm.insertOrUpdate(scalarTokenEntity) + } + } + + suspend fun clearToken(apiUrl: String) { + monarchy.awaitTransaction { realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst()?.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..050f0ba2955583f96881a308237b2cd59e6315aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS, + init: (ConfigurableTask.Builder<PARAMS, RESULT>.() -> Unit) = {} +): ConfigurableTask<PARAMS, RESULT> { + return ConfigurableTask.Builder(this, params).apply(init).build() +} + +internal fun <RESULT> Task<Unit, RESULT>.configureWith(init: (ConfigurableTask.Builder<Unit, RESULT>.() -> Unit) = {}): ConfigurableTask<Unit, RESULT> { + return configureWith(Unit, init) +} + +internal data class ConfigurableTask<PARAMS, RESULT>( + val task: Task<PARAMS, RESULT>, + val params: PARAMS, + val id: UUID, + val callbackThread: TaskThread, + val executionThread: TaskThread, + val callback: MatrixCallback<RESULT> + +) : Task<PARAMS, RESULT> by task { + + class Builder<PARAMS, RESULT>( + private val task: Task<PARAMS, RESULT>, + private val params: PARAMS, + var id: UUID = UUID.randomUUID(), + var callbackThread: TaskThread = TaskThread.MAIN, + var executionThread: TaskThread = TaskThread.IO, + var retryCount: Int = 0, + var callback: MatrixCallback<RESULT> = NoOpMatrixCallback() + ) { + + fun build() = ConfigurableTask( + task = task, + params = params, + id = id, + callbackThread = callbackThread, + executionThread = executionThread, + callback = callback + ) + } + + fun executeBy(taskExecutor: TaskExecutor): Cancelable { + return taskExecutor.execute(this) + } + + override fun toString(): String { + return "${task.javaClass.name} with ID: $id" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt new file mode 100644 index 0000000000000000000000000000000000000000..2fde8478ec1004a8113dc4d3651acfc8b4706d38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +/** + * This class intends to be used to ensure suspendable methods are played sequentially all the way long. + */ +internal interface CoroutineSequencer { + /** + * @param block the suspendable block to execute + * @return the result of the block + */ + suspend fun <T> post(block: suspend () -> T): T +} + +internal open class SemaphoreCoroutineSequencer : CoroutineSequencer { + + // Permits 1 suspend function at a time. + private val semaphore = Semaphore(1) + + override suspend fun <T> post(block: suspend () -> T): T { + return semaphore.withPermit { + block() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..233d50c6953ec7f09575ce16b5ec3aa99f17d81c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.toCancelable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal fun <T> CoroutineScope.launchToCallback( + context: CoroutineContext = EmptyCoroutineContext, + callback: MatrixCallback<T>, + block: suspend () -> T +): Cancelable = launch(context, CoroutineStart.DEFAULT) { + val result = runCatching { + block() + } + withContext(Dispatchers.Main) { + result.foldToCallback(callback) + } +}.toCancelable() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9e7ab2d73f2269362942f298f9b39e5d978918e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +internal interface Task<PARAMS, RESULT> { + + suspend fun execute(params: PARAMS): RESULT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3c815bbe800164da58caca61863f536f6ac05b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.toCancelable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.EmptyCoroutineContext + +@MatrixScope +internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + val executorScope = CoroutineScope(SupervisorJob()) + + fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable { + return executorScope + .launch(task.callbackThread.toDispatcher()) { + val resultOrFailure = runCatching { + withContext(task.executionThread.toDispatcher()) { + Timber.v("Enqueue task $task") + Timber.v("Execute task $task on ${Thread.currentThread().name}") + task.execute(task.params) + } + } + resultOrFailure + .onFailure { + Timber.e(it, "Task failed") + } + .foldToCallback(task.callback) + } + .toCancelable() + } + + fun cancelAll() = executorScope.coroutineContext.cancelChildren() + + private fun TaskThread.toDispatcher() = when (this) { + TaskThread.MAIN -> coroutineDispatchers.main + TaskThread.COMPUTATION -> coroutineDispatchers.computation + TaskThread.IO -> coroutineDispatchers.io + TaskThread.CALLER -> EmptyCoroutineContext + TaskThread.CRYPTO -> coroutineDispatchers.crypto + TaskThread.DM_VERIF -> coroutineDispatchers.dmVerif + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b9c69bccb9057187eb762f1df99d9edc261aa14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +internal enum class TaskThread { + MAIN, + COMPUTATION, + IO, + CALLER, + CRYPTO, + DM_VERIF +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a15f09719f8bc32097f9cce9e1586f9cfd4cec7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +/** + * To be attached to ProcessLifecycleOwner lifecycle + */ +@MatrixScope +internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver { + + var isInBackground: Boolean = false + private set + + private + val listeners = LinkedHashSet<Listener>() + + fun register(listener: Listener) { + listeners.add(listener) + } + + fun unregister(listener: Listener) { + listeners.remove(listener) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onMoveToForeground() { + Timber.v("App returning to foreground…") + isInBackground = false + listeners.forEach { it.onMoveToForeground() } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onMoveToBackground() { + Timber.v("App going to background…") + isInBackground = true + listeners.forEach { it.onMoveToBackground() } + } + + interface Listener { + fun onMoveToForeground() + fun onMoveToBackground() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt new file mode 100644 index 0000000000000000000000000000000000000000..beede697592b5db211bac3d0289529e1f588695b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.util.Cancelable +import kotlinx.coroutines.Job + +internal fun Job.toCancelable(): Cancelable { + return CancelableCoroutine(this) +} + +/** + * Private, use the extension above + */ +private class CancelableCoroutine(private val job: Job) : Cancelable { + + override fun cancel() { + if (!job.isCancelled) { + job.cancel() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt new file mode 100644 index 0000000000000000000000000000000000000000..fccfda15e58c0ee11fcfea9b7a7df762958c299b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.work.WorkManager +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +internal class CancelableWork(private val workManager: WorkManager, + private val workId: UUID) : Cancelable { + + override fun cancel() { + workManager.cancelWorkById(workId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt new file mode 100644 index 0000000000000000000000000000000000000000..0836d96af9aca30e263c68d81fdae652884d9fe8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.util + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.preference.PreferenceManager +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.annotation.RequiresApi +import androidx.core.content.edit +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.math.BigInteger +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.UnrecoverableKeyException +import java.security.cert.CertificateException +import java.security.spec.AlgorithmParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.Calendar +import java.util.zip.GZIPOutputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.IllegalBlockSizeException +import javax.crypto.KeyGenerator +import javax.crypto.NoSuchPaddingException +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.security.auth.x500.X500Principal + +object CompatUtil { + private val TAG = CompatUtil::class.java.simpleName + private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore" + private const val AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding" + private const val AES_GCM_KEY_SIZE_IN_BITS = 128 + private const val AES_GCM_IV_LENGTH = 12 + private const val AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection" + + private const val RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection" + private const val RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding" + private const val AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection" + + private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated" + + private var sSecretKeyAndVersion: SecretKeyAndVersion? = null + + /** + * Returns the unique SecureRandom instance shared for all local storage encryption operations. + */ + private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() } + + /** + * Create a GZIPOutputStream instance + * Special treatment on KitKat device, force the syncFlush param to false + * Before Kitkat, this param does not exist and after Kitkat it is set to false by default + * + * @param outputStream the output stream + */ + @Throws(IOException::class) + fun createGzipOutputStream(outputStream: OutputStream): GZIPOutputStream { + return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + GZIPOutputStream(outputStream, false) + } else { + GZIPOutputStream(outputStream) + } + } + + /** + * Returns the AES key used for local storage encryption/decryption with AES/GCM. + * The key is created if it does not exist already in the keystore. + * From Marshmallow, this key is generated and operated directly from the android keystore. + * From KitKat and before Marshmallow, this key is stored in the application shared preferences + * wrapped by a RSA key generated and operated directly from the android keystore. + * + * @param context the context holding the application shared preferences + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Synchronized + @Throws(KeyStoreException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + IOException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + InvalidKeyException::class, + IllegalBlockSizeException::class, + UnrecoverableKeyException::class) + private fun getAesGcmLocalProtectionKey(context: Context): SecretKeyAndVersion { + if (sSecretKeyAndVersion == null) { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER) + keyStore.load(null) + + Timber.i(TAG, "Loading local protection key") + + var key: SecretKey? + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + // Get the version of Android when the key has been generated, default to the current version of the system. In this case, the + // key will be generated + val androidVersionWhenTheKeyHasBeenGenerated = sharedPreferences + .getInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "AES local protection key found in keystore") + key = keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null) as SecretKey + } else { + // Check if a key has been created on version < M (in case of OS upgrade) + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating AES key with keystore") + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER) + generator.init( + KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setKeySize(AES_GCM_KEY_SIZE_IN_BITS) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()) + key = generator.generateKey() + + sharedPreferences.edit { + putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + } + } + } + } else { + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating RSA key pair with keystore") + val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE_PROVIDER) + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 10) + + generator.initialize( + KeyPairGeneratorSpec.Builder(context) + .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS) + .setSubject(X500Principal("CN=matrix-android-sdk")) + .setStartDate(start.time) + .setEndDate(end.time) + .setSerialNumber(BigInteger.ONE) + .build()) + val keyPair = generator.generateKeyPair() + + Timber.i(TAG, "Generating wrapped AES key") + + val aesKeyRaw = ByteArray(AES_GCM_KEY_SIZE_IN_BITS / java.lang.Byte.SIZE) + prng.nextBytes(aesKeyRaw) + key = SecretKeySpec(aesKeyRaw, "AES") + + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.WRAP_MODE, keyPair.public) + val wrappedAesKey = cipher.wrap(key) + + sharedPreferences.edit { + putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) + putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + } + } + } + + sSecretKeyAndVersion = SecretKeyAndVersion(key!!, androidVersionWhenTheKeyHasBeenGenerated) + } + + return sSecretKeyAndVersion!! + } + + /** + * Read the key, which may have been stored when the OS was < M + * + * @param sharedPreferences shared pref + * @param keyStore key store + * @return the key if it exists or null + */ + @Throws(KeyStoreException::class, + NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + InvalidKeyException::class, + UnrecoverableKeyException::class) + private fun readKeyApiL(sharedPreferences: SharedPreferences, keyStore: KeyStore): SecretKey? { + val wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null) + if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "RSA + wrapped AES local protection keys found in keystore") + val privateKey = keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null) as PrivateKey + val wrappedAesKey = Base64.decode(wrappedAesKeyString, 0) + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.UNWRAP_MODE, privateKey) + return cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY) as SecretKey + } + + // Key does not exist + return null + } + + /** + * Create a CipherOutputStream instance. + * Before Kitkat, this method will return out as local storage encryption is not implemented for + * devices before KitKat. + * + * @param out the output stream + * @param context the context holding the application shared preferences + */ + @Throws(IOException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + UnrecoverableKeyException::class, + InvalidKeyException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + NoSuchProviderException::class, + KeyStoreException::class, + IllegalBlockSizeException::class) + fun createCipherOutputStream(out: OutputStream, context: Context): OutputStream? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return out + } + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + val iv: ByteArray + + if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey) + iv = cipher.iv + } else { + iv = ByteArray(AES_GCM_IV_LENGTH) + prng.nextBytes(iv) + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey, IvParameterSpec(iv)) + } + + if (iv.size != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length ${iv.size}") + return null + } + + out.write(iv.size) + out.write(iv) + + return CipherOutputStream(out, cipher) + } + + /** + * Create a CipherInputStream instance. + * Before Kitkat, this method will return `in` because local storage encryption is not implemented for devices before KitKat. + * Warning, if `in` is not an encrypted stream, it's up to the caller to close and reopen `in`, because the stream has been read. + * + * @param in the input stream + * @param context the context holding the application shared preferences + * @return in, or the created InputStream, or null if the InputStream `in` does not contain encrypted data + */ + @Throws(NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + CertificateException::class, + InvalidKeyException::class, + KeyStoreException::class, + UnrecoverableKeyException::class, + IllegalBlockSizeException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + IOException::class) + fun createCipherInputStream(`in`: InputStream, context: Context): InputStream? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return `in` + } + + val iv_len = `in`.read() + if (iv_len != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length $iv_len") + return null + } + + val iv = ByteArray(AES_GCM_IV_LENGTH) + `in`.read(iv) + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + + val spec: AlgorithmParameterSpec = if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv) + } else { + IvParameterSpec(iv) + } + + cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.secretKey, spec) + + return CipherInputStream(`in`, cipher) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt new file mode 100644 index 0000000000000000000000000000000000000000..46ba75968ccdaff3bec44aef01080e355a212a23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import android.os.Handler + +internal class Debouncer(private val handler: Handler) { + + private val runnables = HashMap<String, Runnable>() + + fun debounce(identifier: String, r: Runnable, millis: Long): Boolean { + // debounce + runnables[identifier]?.let { runnable -> handler.removeCallbacks(runnable) } + + insertRunnable(identifier, r, millis) + return true + } + + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { + val chained = Runnable { + handler.post(r) + runnables.remove(identifier) + } + runnables[identifier] = chained + handler.postDelayed(chained, millis) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt new file mode 100644 index 0000000000000000000000000000000000000000..eaf17b9ae073e0297117a2631dcc2c6722f27e37 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +// Trick to ensure that when block is exhaustive +internal val <T> T.exhaustive: T get() = this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt new file mode 100644 index 0000000000000000000000000000000000000000..27625d90bc990d5de2d1afd184e063790653ba56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.annotation.WorkerThread +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +/** + * Save an input stream to a file with Okio + */ +@WorkerThread +fun writeToFile(inputStream: InputStream, outputFile: File) { + FileOutputStream(outputFile).use { + inputStream.copyTo(it) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d103e1031a55088d186ed51029ba2c6ec018840 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +internal fun createBackgroundHandler(name: String): Handler = Handler( + HandlerThread(name).apply { start() }.looper +) + +internal fun createUIHandler(): Handler = Handler( + Looper.getMainLooper() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8c22afb30b0c8dd7ddafcb3d1ce1c252e277b8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import java.security.MessageDigest + +/** + * Compute a Hash of a String, using md5 algorithm + */ +fun String.md5() = try { + val digest = MessageDigest.getInstance("md5") + digest.update(toByteArray()) + digest.digest() + .joinToString("") { String.format("%02X", it) } + .toLowerCase() +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt new file mode 100644 index 0000000000000000000000000000000000000000..4a15f2ff983e8e490cb27147d1d212587215319f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.annotation.VisibleForTesting +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber +import java.util.TreeSet + +/** + * Build canonical Json + * Doc: https://matrix.org/docs/spec/appendices.html#canonical-json + */ +object JsonCanonicalizer { + + fun <T> getCanonicalJson(type: Class<T>, o: T): String { + val adapter = MoshiProvider.providesMoshi().adapter<T>(type) + + // Canonicalize manually + return canonicalize(adapter.toJson(o)) + .replace("\\/", "/") + } + + @VisibleForTesting + fun canonicalize(jsonString: String): String { + return try { + val jsonObject = JSONObject(jsonString) + + canonicalizeRecursive(jsonObject) + } catch (e: JSONException) { + Timber.e(e, "Unable to canonicalize") + jsonString + } + } + + /** + * Canonicalize a JSON element + * + * @param src the src + * @return the canonicalize element + */ + private fun canonicalizeRecursive(any: Any): String { + when (any) { + is JSONArray -> { + // Canonicalize each element of the array + return (0 until any.length()).joinToString(separator = ",", prefix = "[", postfix = "]") { + canonicalizeRecursive(any.get(it)) + } + } + is JSONObject -> { + // Sort the attributes by name, and the canonicalize each element of the JSONObject + + val attributes = TreeSet<String>() + for (entry in any.keys()) { + attributes.add(entry) + } + + return buildString { + append("{") + for ((index, value) in attributes.withIndex()) { + append("\"") + append(value) + append("\"") + append(":") + append(canonicalizeRecursive(any[value])) + + if (index < attributes.size - 1) { + append(",") + } + } + append("}") + } + } + is String -> return JSONObject.quote(any) + else -> return any.toString() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c38729fe46e91e471653e242aa5300fdb2a7cb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +object LiveDataUtils { + + fun <FIRST, SECOND, OUT> combine(firstSource: LiveData<FIRST>, + secondSource: LiveData<SECOND>, + mapper: (FIRST, SECOND) -> OUT): LiveData<OUT> { + return MediatorLiveData<OUT>().apply { + var firstValue: FIRST? = null + var secondValue: SECOND? = null + + val valueDispatcher = { + firstValue?.let { safeFirst -> + secondValue?.let { safeSecond -> + val mappedValue = mapper(safeFirst, safeSecond) + postValue(mappedValue) + } + } + } + + addSource(firstSource) { + firstValue = it + valueDispatcher() + } + + addSource(secondSource) { + secondValue = it + valueDispatcher() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt new file mode 100644 index 0000000000000000000000000000000000000000..d66a38d346fa4f2fb6ef95671348042900b2a8c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import kotlinx.coroutines.CoroutineDispatcher + +internal data class MatrixCoroutineDispatchers( + val io: CoroutineDispatcher, + val computation: CoroutineDispatcher, + val main: CoroutineDispatcher, + val crypto: CoroutineDispatcher, + val dmVerif: CoroutineDispatcher +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt new file mode 100644 index 0000000000000000000000000000000000000000..81f5af9ac61aa310b0e78867b3ee8352f084a295 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.awaitTransaction +import io.realm.Realm +import io.realm.RealmModel +import java.util.concurrent.atomic.AtomicReference + +internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { + return awaitTransaction(realmConfiguration, transaction) +} + +fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? { + val ref = AtomicReference<T>() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + realm.copyFromRealm(it) + } + ref.set(result) + } + return ref.get() +} + +fun <U, T : RealmModel> Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? { + val ref = AtomicReference<U?>() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + map(realm.copyFromRealm(it), realm) + } + ref.set(result) + } + return ref.get() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt new file mode 100644 index 0000000000000000000000000000000000000000..d96be91618f9c46060a563136ab78762721f7f1c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import javax.crypto.SecretKey + +/** + * Tuple which contains the secret key and the version of Android when the key has been generated + */ +internal data class SecretKeyAndVersion( + /** + * the key + */ + val secretKey: SecretKey, + /** + * The android version when the key has been generated + */ + val androidVersionWhenTheKeyHasBeenGenerated: Int) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..902d7d33161ac9ae99dfeb7a7adda61fb449c172 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import android.content.res.Resources +import androidx.annotation.ArrayRes +import androidx.annotation.NonNull +import androidx.annotation.StringRes +import dagger.Reusable +import javax.inject.Inject + +@Reusable +internal class StringProvider @Inject constructor(private val resources: Resources) { + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + */ + @NonNull + fun getString(@StringRes resId: Int): String { + return resources.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ + @NonNull + fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return resources.getString(resId, *formatArgs) + } + + @Throws(Resources.NotFoundException::class) + fun getStringArray(@ArrayRes id: Int): Array<String> { + return resources.getStringArray(id) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..a236771cd65ec7a07ae13b023088611ecb1132d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import timber.log.Timber + +/** + * Convert a string to an UTF8 String + * + * @param s the string to convert + * @return the utf-8 string + */ +fun convertToUTF8(s: String): String { + return try { + val bytes = s.toByteArray(Charsets.UTF_8) + String(bytes) + } catch (e: Exception) { + Timber.e(e, "## convertToUTF8() failed") + s + } +} + +/** + * Convert a string from an UTF8 String + * + * @param s the string to convert + * @return the utf-16 string + */ +fun convertFromUTF8(s: String): String { + return try { + val bytes = s.toByteArray() + String(bytes, Charsets.UTF_8) + } catch (e: Exception) { + Timber.e(e, "## convertFromUTF8() failed") + s + } +} + +fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..0595d68c3b2ff6e4b87223e20ca2bf5d7f73f8f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt @@ -0,0 +1,36 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.MatrixCallback +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend inline fun <T> awaitCallback(crossinline callback: (MatrixCallback<T>) -> Unit) = suspendCoroutine<T> { cont -> + callback(object : MatrixCallback<T> { + override fun onFailure(failure: Throwable) { + cont.resumeWithException(failure) + } + + override fun onSuccess(data: T) { + cont.resume(data) + } + }) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..da155c8bdd2c5090de1b2ab44b6ce99d77154858 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import java.net.URL + +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} + +/** + * Ensure string has trailing / + */ +internal fun String.ensureTrailingSlash(): String { + return when { + isEmpty() -> this + !endsWith("/") -> "$this/" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e20fe9a30453c24647187c75f00d57ab57729d22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.WellKnown +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.session.homeserver.CapabilitiesAPI +import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task<GetWellknownTask.Params, WellknownResult> { + data class Params( + val matrixId: String, + val homeServerConnectionConfig: HomeServerConnectionConfig? + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + val client = buildClient(params.homeServerConnectionConfig) + return findClientConfig(homeServerDomain, client) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig?): OkHttpClient { + return if (homeServerConnectionConfig != null) { + okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } else { + okHttpClient.get() + } + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult { + val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest<WellKnown>(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown, client) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is UnrecognizedCertificateException -> { + throw Failure.UnrecognizedCertificateFailure( + "https://$domain", + throwable.fingerprint + ) + } + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown, client: OkHttpClient): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(client, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest<Unit>(null) { + apiCall = capabilitiesAPI.ping() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl, client)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String, client: OkHttpClient): Boolean { + val identityPingApi = retrofitFactory.create(client, identityServerBaseUrl) + .create(IdentityAuthAPI::class.java) + + return try { + executeRequest<Unit>(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback<String?>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback<WellKnown>(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback<List<WellKnownManagerConfig>>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback<WellKnown>(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d88614809b82eb08de8b1526a79f8e8b4e5c4af --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.wellknown + +import org.matrix.android.sdk.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call<WellKnown> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab422237554696af13649d3091f2044d65d5218e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.wellknown + +import dagger.Binds +import dagger.Module + +@Module +internal abstract class WellknownModule { + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0fc7df0989e0a05c90275a6a4bcd6e9d06b6af7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters) + : Worker(context, params) { + + override fun doWork(): Result { + return Result.success() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..e711b0d68624f8f8197f9c39ef22cd01f79930a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters + +interface DelegateWorkerFactory { + + fun create(context: Context, params: WorkerParameters): ListenableWorker +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f824a76b8f8e804f79a1418ce1951846f535e9e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.internal.session.room.send.NoMerger + +/** + * If startChain parameter is true, the builder will have a inputMerger set to [NoMerger] + */ +internal fun OneTimeWorkRequest.Builder.startChain(startChain: Boolean): OneTimeWorkRequest.Builder { + if (startChain) { + setInputMerger(NoMerger::class.java) + } + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..509cecf02239ce3b9e8b75a6a1091a4bf4eac976 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import javax.inject.Inject +import javax.inject.Provider + +class MatrixWorkerFactory @Inject constructor( + private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<DelegateWorkerFactory>> +) : WorkerFactory() { + + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + val foundEntry = + workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) } + val factoryProvider = foundEntry?.value + ?: throw IllegalArgumentException("unknown worker class name: $workerClassName") + return factoryProvider.get().create(appContext, workerParameters) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..840cda3dec422a289530ff531849975dbb75010a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +/** + * Note about the Worker usage: + * The workers we chain, or when using the append strategy, should never return Result.Failure(), else the chain will be broken forever + */ +interface SessionWorkerParams { + val sessionId: String + + /** + * Null when no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers + * If it is the case, the worker should just transmit the error and shouldn't do anything else + */ + val lastFailureMessage: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt new file mode 100644 index 0000000000000000000000000000000000000000..9f40d6aa0509ea8270e858a1a7fe8a38c8b66847 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.ListenableWorker +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.internal.session.SessionComponent + +internal fun ListenableWorker.getSessionComponent(sessionId: String): SessionComponent? { + return Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(sessionId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b7cba0b0cadbe7f99487063da5d0b6d65a5eb38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.Data +import org.matrix.android.sdk.internal.di.MoshiProvider + +object WorkerParamsFactory { + + const val KEY = "WORKER_PARAMS_JSON" + + inline fun <reified T> toData(params: T): Data { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(T::class.java) + val json = adapter.toJson(params) + return Data.Builder().putString(KEY, json).build() + } + + inline fun <reified T> fromData(data: Data): T? { + val json = data.getString(KEY) + return if (json == null) { + null + } else { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(T::class.java) + adapter.fromJson(json) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java new file mode 100755 index 0000000000000000000000000000000000000000..3811cf65cd7ca4bfd4fbb961ab6f7f7f03615b4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.androidsdk.crypto.data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +public class MXDeviceInfo implements Serializable { + private static final long serialVersionUID = 20129670646382964L; + + // This device is a new device and the user was not warned it has been added. + public static final int DEVICE_VERIFICATION_UNKNOWN = -1; + + // The user has not yet verified this device. + public static final int DEVICE_VERIFICATION_UNVERIFIED = 0; + + // The user has verified this device. + public static final int DEVICE_VERIFICATION_VERIFIED = 1; + + // The user has blocked this device. + public static final int DEVICE_VERIFICATION_BLOCKED = 2; + + /** + * The id of this device. + */ + public String deviceId; + + /** + * the user id + */ + public String userId; + + /** + * The list of algorithms supported by this device. + */ + public List<String> algorithms; + + /** + * A map from <key type>:<id> to <base64-encoded key>>. + */ + public Map<String, String> keys; + + /** + * The signature of this MXDeviceInfo. + * A map from <key type>:<device_id> to <base64-encoded key>>. + */ + public Map<String, Map<String, String>> signatures; + + /* + * Additional data from the home server. + */ + public Map<String, Object> unsigned; + + /** + * Verification state of this device. + */ + public int mVerified; + + /** + * Constructor + */ + public MXDeviceInfo() { + mVerified = DEVICE_VERIFICATION_UNKNOWN; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java new file mode 100755 index 0000000000000000000000000000000000000000..51c1fe2f1e7a194e078b93bcb2cd19f355390d2f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.androidsdk.crypto.data; + +import org.matrix.olm.OlmInboundGroupSession; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * This class adds more context to a OLMInboundGroupSession object. + * This allows additional checks. The class implements NSCoding so that the context can be stored. + */ +public class MXOlmInboundGroupSession2 implements Serializable { + // define a serialVersionUID to avoid having to redefine the class after updates + private static final long serialVersionUID = 201702011617L; + + // The associated olm inbound group session. + public OlmInboundGroupSession mSession; + + // The room in which this session is used. + public String mRoomId; + + // The base64-encoded curve25519 key of the sender. + public String mSenderKey; + + // Other keys the sender claims. + public Map<String, String> mKeysClaimed; + + // Devices which forwarded this session to us (normally empty). + public List<String> mForwardingCurve25519KeyChain = new ArrayList<>(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml new file mode 100644 index 0000000000000000000000000000000000000000..72026cd7a031fa1a3ec46d668a6c920eb538700d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M30,23.828c-0.391,0.392 -1.023,0.392 -1.414,0l-1.414,-1.414c-0.392,-0.391 -0.392,-1.024 0,-1.414L30,18.172c0.391,-0.391 1.023,-0.391 1.414,0l1.414,1.414c0.392,0.391 0.392,1.024 0,1.414L30,23.828zM15,8.828c-0.391,0.392 -1.023,0.392 -1.414,0l-1.414,-1.414c-0.392,-0.391 -0.392,-1.023 0,-1.414L15,3.172c0.391,-0.391 1.023,-0.391 1.414,0l1.414,1.414c0.392,0.391 0.392,1.023 0,1.414L15,8.828z" + android:fillColor="#66757F"/> + <path + android:pathData="M2,22c2,0 11,1 11,1s1,9 1,11 -2,2 -3,1 -4,-6 -4,-6 -5,-3 -6,-4 -1,-3 1,-3zM4,6.039C7,6 29,7 29,7s0.924,22 0.962,25c0.038,3 -2.763,4.002 -3.862,0.001S21,15 21,15 7.045,10.583 3.995,9.898C0,9 0.999,6.077 4,6.039z" + android:fillColor="#55ACEE"/> + <path + android:pathData="M27,3c2,-2 7,-3 8,-2s0,6 -2,8 -19,18 -19,18 -6.5,4.5 -8,3 3,-8 3,-8S25,5 27,3z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M14,22s0.5,0.5 -4,5 -5,4 -5,4 -0.5,-0.5 4,-5 5,-4 5,-4zM29,4c1.657,0 3,1.343 3,3h0.805c0.114,-0.315 0.195,-0.645 0.195,-1 0,-1.657 -1.343,-3 -3,-3 -0.355,0 -0.685,0.081 -1,0.195V4z" + android:fillColor="#66757F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml new file mode 100644 index 0000000000000000000000000000000000000000..b89d033b9e122c3bf4168ae0fa3f6b0b3cb43d11 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M30.5,18.572L26,25h2.575c-1.13,3.988 -4.445,7.05 -8.575,7.81V17h3c1.104,0 2,-0.896 2,-2s-0.896,-2 -2,-2h-3v-1.349h-4V13h-3c-1.104,0 -2,0.896 -2,2s0.896,2 2,2h3v15.81c-4.13,-0.76 -7.445,-3.821 -8.575,-7.81H10l-4.5,-6.428L1,25h3.33C5.705,31.289 11.299,36 18,36s12.295,-4.711 13.67,-11H35l-4.5,-6.428z" + android:fillColor="#269"/> + <path + android:pathData="M18,0c-3.314,0 -6,2.686 -6,6s2.686,6 6,6 6,-2.686 6,-6 -2.686,-6 -6,-6zM18,9c-1.657,0 -3,-1.343 -3,-3s1.343,-3 3,-3 3,1.343 3,3 -1.343,3 -3,3z" + android:fillColor="#269"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml new file mode 100644 index 0000000000000000000000000000000000000000..54e0f9a3c0b7eb8195ce18294d9dcc4995ca44fc --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M24,7c-3,0 -3,1 -6,1s-3,-1 -6,-1c-4,0 -9,2 -9,9 0,11 6,20 10,20 3,0 3,-1 5,-1s2,1 5,1c4,0 10,-9 10,-20 0,-7.001 -5,-9 -9,-9z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M19,7s3,-4 8,-4c4,0 6,2 6,2s-4,3 -7,3 -7,-1 -7,-1z" + android:fillColor="#77B255"/> + <path + android:pathData="M18,10c-0.552,0 -1,-0.448 -1,-1 0,-3.441 1.2,-6.615 3.293,-8.707 0.391,-0.391 1.023,-0.391 1.414,0s0.391,1.024 0,1.414C19.986,3.427 19,6.085 19,9c0,0.552 -0.448,1 -1,1z" + android:fillColor="#662113"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml new file mode 100644 index 0000000000000000000000000000000000000000..b12c6d245bce3456aa3242df230429b06e871121 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="#F5F8FA"/> + <path + android:pathData="M18,11c-0.552,0 -1,-0.448 -1,-1L17,3c0,-0.552 0.448,-1 1,-1s1,0.448 1,1v7c0,0.552 -0.448,1 -1,1zM11.417,15.5c-0.1,0 -0.202,-0.015 -0.302,-0.047l-8.041,-2.542c-0.527,-0.167 -0.819,-0.728 -0.652,-1.255 0.166,-0.527 0.73,-0.818 1.255,-0.652l8.042,2.542c0.527,0.167 0.819,0.729 0.652,1.255 -0.136,0.426 -0.53,0.699 -0.954,0.699zM25.042,15.209c-0.434,0 -0.833,-0.285 -0.96,-0.722 -0.154,-0.531 0.151,-1.085 0.682,-1.239l6.75,-1.958c0.531,-0.153 1.085,0.153 1.238,0.682 0.154,0.531 -0.151,1.085 -0.682,1.239l-6.75,1.958c-0.092,0.027 -0.186,0.04 -0.278,0.04zM27.043,30.167c-0.306,0 -0.606,-0.14 -0.803,-0.403l-5.459,-7.333c-0.33,-0.442 -0.238,-1.069 0.205,-1.399 0.442,-0.331 1.069,-0.238 1.399,0.205l5.459,7.333c0.33,0.442 0.238,1.069 -0.205,1.399 -0.179,0.134 -0.389,0.198 -0.596,0.198zM8.749,30.084c-0.197,0 -0.395,-0.058 -0.57,-0.179 -0.454,-0.316 -0.565,-0.938 -0.25,-1.392l5.125,-7.375c0.315,-0.454 0.938,-0.566 1.392,-0.251 0.454,0.315 0.565,0.939 0.25,1.392l-5.125,7.375c-0.194,0.281 -0.506,0.43 -0.822,0.43zM3.5,27.062c-0.44,0 -0.844,-0.293 -0.965,-0.738L0.347,18.262c-0.145,-0.533 0.17,-1.082 0.704,-1.227 0.535,-0.141 1.083,0.171 1.227,0.704l2.188,8.062c0.145,0.533 -0.17,1.082 -0.704,1.226 -0.088,0.025 -0.176,0.035 -0.262,0.035zM22,34h-9c-0.552,0 -1,-0.447 -1,-1s0.448,-1 1,-1h9c0.553,0 1,0.447 1,1s-0.447,1 -1,1zM32.126,27.125c-0.079,0 -0.16,-0.009 -0.24,-0.029 -0.536,-0.132 -0.864,-0.674 -0.731,-1.21l2.125,-8.625c0.133,-0.536 0.679,-0.862 1.21,-0.732 0.536,0.132 0.864,0.674 0.731,1.211l-2.125,8.625c-0.113,0.455 -0.521,0.76 -0.97,0.76zM30.312,7.688c-0.17,0 -0.342,-0.043 -0.5,-0.134L22.25,3.179c-0.478,-0.277 -0.642,-0.888 -0.364,-1.367 0.275,-0.478 0.886,-0.643 1.366,-0.365l7.562,4.375c0.478,0.277 0.642,0.888 0.364,1.367 -0.185,0.32 -0.521,0.499 -0.866,0.499zM5.501,7.688c-0.312,0 -0.618,-0.145 -0.813,-0.417 -0.322,-0.45 -0.22,-1.074 0.229,-1.396l6.188,-4.438c0.449,-0.322 1.074,-0.219 1.396,0.229 0.322,0.449 0.219,1.074 -0.229,1.396L6.083,7.5c-0.177,0.126 -0.38,0.188 -0.582,0.188z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M25.493,13.516l-7.208,-5.083c-0.348,-0.245 -0.814,-0.243 -1.161,0.006l-7.167,5.167c-0.343,0.248 -0.494,0.684 -0.375,1.091l2.5,8.583c0.124,0.426 0.515,0.72 0.96,0.72L22,24c0.43,0 0.81,-0.274 0.948,-0.681l2.917,-8.667c0.141,-0.419 -0.011,-0.881 -0.372,-1.136zM1.292,19.542c0.058,0 0.117,-0.005 0.175,-0.016 0.294,-0.052 0.55,-0.233 0.697,-0.494l3.375,-6c0.051,-0.091 0.087,-0.188 0.108,-0.291L6.98,6.2c0.06,-0.294 -0.016,-0.6 -0.206,-0.832C6.584,5.135 6.3,5 6,5h-0.428C2.145,8.277 0,12.884 0,18c0,0.266 0.028,0.525 0.04,0.788l0.602,0.514c0.182,0.156 0.413,0.24 0.65,0.24zM10.617,2.995c0.106,0.219 0.313,0.373 0.553,0.412l6.375,1.042c0.04,0.006 0.081,0.01 0.121,0.01 0.04,0 0.081,-0.003 0.122,-0.01l6.084,-1c0.2,-0.033 0.38,-0.146 0.495,-0.314 0.116,-0.168 0.158,-0.375 0.118,-0.575l-0.292,-1.443C22.26,0.407 20.18,0 18,0c-2.425,0 -4.734,0.486 -6.845,1.356l-0.521,0.95c-0.117,0.213 -0.123,0.47 -0.017,0.689zM31.134,5.719l-1.504,-0.095c-0.228,-0.013 -0.455,0.076 -0.609,0.249 -0.152,0.173 -0.218,0.402 -0.175,0.63l1.167,6.198c0.017,0.086 0.048,0.148 0.093,0.224 1.492,2.504 3.152,5.301 3.381,5.782 0.024,0.084 0.062,0.079 0.114,0.151 0.14,0.195 0.372,0.142 0.612,0.142h0.007c0.198,0 0.323,0.094 1.768,-0.753 0.001,-0.083 0.012,-0.164 0.012,-0.247 0,-4.753 -1.856,-9.064 -4.866,-12.281zM14.541,33.376c0.011,-0.199 -0.058,-0.395 -0.191,-0.544l-4.5,-5c-0.06,-0.066 -0.131,-0.122 -0.211,-0.163 -5.885,-3.069 -5.994,-3.105 -6.066,-3.13 -0.078,-0.025 -0.161,-0.039 -0.242,-0.039 -0.537,0 -0.695,0.065 -1.185,2.024 2.236,4.149 6.053,7.316 10.644,8.703l1.5,-1.333c0.149,-0.132 0.239,-0.319 0.251,-0.518zM32.374,24.809c-0.189,-0.08 -0.405,-0.078 -0.592,0.005l-6.083,2.667c-0.106,0.046 -0.2,0.116 -0.274,0.205l-4.25,5.083c-0.129,0.154 -0.19,0.352 -0.172,0.552 0.02,0.2 0.117,0.384 0.272,0.51 0.683,0.559 1.261,1.03 1.767,1.44 4.437,-1.294 8.154,-4.248 10.454,-8.146l-0.712,-1.889c-0.072,-0.193 -0.221,-0.347 -0.41,-0.427z" + android:fillColor="#31373D"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml new file mode 100644 index 0000000000000000000000000000000000000000..cdd3cb1b9f5b12bd0d51443523ddf64d617d85b2 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml @@ -0,0 +1,36 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M28,2c2.684,-1.342 5,4 3,13 -1.106,4.977 -5,9 -9,12s-11,-1 -7,-5 8,-7 10,-13c1.304,-3.912 1,-6 3,-7z" + android:fillColor="#FFE8B6"/> + <path + android:pathData="M31,8c0,3 -1,9 -4,13s-7,5 -4,1 5,-7 6,-11 2,-7 2,-3z" + android:fillColor="#FFD983"/> + <path + android:pathData="M22,20c-0.296,0.592 1.167,-3.833 -3,-6 -1.984,-1.032 -10,1 -4,1 3,0 4,2 2,4 -0.291,0.292 -0.489,0.603 -0.622,0.912 -0.417,0.346 -0.873,0.709 -1.378,1.088 -2.263,1.697 -5.84,4.227 -10,7 -3,2 -4,3 -4,4 0,3 9,3 14,1s10,-7 10,-7l4,-4c-3,-4 -7,-2 -7,-2z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M22,20s1.792,-4.729 -3,-7c-4.042,-1.916 -8,-1 -11,1s-2,4 -3,5 1,2 3,0 8.316,-4.895 11,-4c3,1 2,2.999 3,5z" + android:fillColor="#FFE8B6"/> + <path + android:pathData="M26,35h-4c-2,0 -3,1 -4,1s-2,-2 0,-2 4,0 5,-1 5,2 3,2z" + android:fillColor="#A6D388"/> + <path + android:pathData="M18,35m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M32.208,28S28,35 26,35h-4c-2,0 0,-1 1,-2s5,0 5,-6c0,-3 4.208,1 4.208,1z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M26,19c3,0 8,3 7,9s-5,7 -7,7h-2c-2,0 -1,-1 0,-2s4,0 4,-6c0,-3 -4,-7 -6,-7 0,0 2,-1 4,-1z" + android:fillColor="#FFE8B6"/> + <path + android:pathData="M17,21c3,0 5,1 3,3 -1.581,1.581 -6,5 -10,6s-8,1 -5,-1 9.764,-8 12,-8z" + android:fillColor="#FFD983"/> + <path + android:pathData="M2,31c1,0 1,0 1,0.667C3,32.333 3,33 2,33s-1,-1.333 -1,-1.333S1,31 2,31z" + android:fillColor="#C1694F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f29828bcf3ebe6d778aae8f8f81941bd5586e36 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M28,13c0,11 5,10 5,15 0,0 0,2 -2,2H5c-2,0 -2,-2 -2,-2 0,-5 5,-4 5,-15C8,7.478 12.477,3 18,3s10,4.478 10,10z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M18,3m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" + android:fillColor="#FFAC33"/> + <path + android:pathData="M18,36c2.209,0 4,-1.791 4,-4h-8c0,2.209 1.791,4 4,4z" + android:fillColor="#FFAC33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml new file mode 100644 index 0000000000000000000000000000000000000000..1427e793c5c5fd9ec64a6812bf37839994e36692 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml @@ -0,0 +1,27 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M7,24c1.957,0 3.633,1.135 4.455,2.772l3.477,-1.739C13.488,22.058 10.446,20 6.916,20c-1.301,0 -2.534,0.285 -3.649,0.787l1.668,3.67C5.566,24.17 6.262,24 7,24zM29,24c1.467,0 2.772,0.643 3.688,1.648l2.897,-2.635C33.952,21.169 31.573,20 28.916,20c-3.576,0 -6.652,2.111 -8.073,5.15l3.648,1.722C25.293,25.18 27.003,24 29,24z" + android:fillColor="#EA596E"/> + <path + android:pathData="M7,22c-3.866,0 -7,3.134 -7,7s3.134,7 7,7 7,-3.134 7,-7 -3.133,-7 -7,-7zM7,34c-2.761,0 -5,-2.238 -5,-5s2.239,-5 5,-5 5,2.238 5,5 -2.238,5 -5,5zM29,22c-3.865,0 -7,3.134 -7,7s3.135,7 7,7c3.867,0 7,-3.134 7,-7s-3.133,-7 -7,-7zM29,34c-2.761,0 -5,-2.238 -5,-5s2.239,-5 5,-5c2.762,0 5,2.238 5,5s-2.238,5 -5,5z" + android:fillColor="#292F33"/> + <path + android:pathData="M29.984,28.922c-0.005,-0.067 -0.021,-0.132 -0.04,-0.198 -0.019,-0.065 -0.04,-0.126 -0.071,-0.186 -0.013,-0.024 -0.015,-0.052 -0.029,-0.075l-7,-11c-0.297,-0.466 -0.914,-0.604 -1.381,-0.307 -0.299,0.19 -0.444,0.513 -0.445,0.843H12c-0.552,0 -1,0.447 -1,1 0,0.553 0.448,1 1,1h10c0.027,0 0.05,-0.014 0.077,-0.016L27.178,28H18c-0.552,0 -1,0.447 -1,1s0.448,1 1,1h11.001c0.116,0 0.23,-0.028 0.343,-0.069 0.034,-0.013 0.064,-0.027 0.097,-0.043 0.031,-0.017 0.066,-0.024 0.097,-0.044 0.03,-0.02 0.048,-0.051 0.075,-0.072 0.055,-0.044 0.103,-0.089 0.147,-0.143 0.041,-0.049 0.074,-0.099 0.104,-0.154 0.03,-0.056 0.055,-0.11 0.075,-0.172 0.021,-0.066 0.033,-0.132 0.04,-0.201 0.004,-0.036 0.021,-0.066 0.021,-0.102 0,-0.027 -0.014,-0.051 -0.016,-0.078z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M21.581,16l-2.899,8.117 -5.929,-6.775c-0.364,-0.415 -0.996,-0.459 -1.411,-0.094 -0.415,0.364 -0.457,0.995 -0.094,1.411l6.664,7.615 -0.854,2.39c-0.185,0.519 0.086,1.092 0.606,1.277 0.111,0.04 0.224,0.059 0.336,0.059 0.411,0 0.796,-0.255 0.942,-0.664L23.705,16h-2.124z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M7,30c-0.15,0 -0.303,-0.034 -0.446,-0.105 -0.494,-0.247 -0.694,-0.848 -0.447,-1.342l3.062,-6.106C9.186,22.419 11,19.651 11,17c0,-3.242 -2.293,-4.043 -2.316,-4.051 -0.524,-0.175 -0.807,-0.741 -0.632,-1.265 0.174,-0.524 0.739,-0.81 1.265,-0.632C9.467,11.102 13,12.333 13,17c0,3.068 -1.836,6.042 -2.131,6.497l-2.974,5.949C7.72,29.798 7.367,30 7,30z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M14.612,13.663c-0.054,0 -0.11,-0.004 -0.165,-0.014l-6,-1c-0.544,-0.091 -0.913,-0.606 -0.822,-1.151 0.091,-0.544 0.601,-0.913 1.151,-0.822l6,1c0.544,0.091 0.913,0.606 0.822,1.151 -0.082,0.489 -0.506,0.836 -0.986,0.836zM26.383,17c-0.03,0 -0.059,-0.002 -0.089,-0.006l-5.672,-0.708c-0.372,-0.046 -0.644,-0.374 -0.62,-0.748 0.023,-0.374 0.333,-0.665 0.707,-0.665 0.041,0 4.067,-0.018 5.989,-1.299 0.25,-0.167 0.582,-0.157 0.824,0.026 0.239,0.185 0.337,0.501 0.241,0.788l-0.709,2.127c-0.096,0.293 -0.369,0.485 -0.671,0.485z" + android:fillColor="#292F33"/> + <path + android:pathData="M20,29c0,1.104 -0.895,2 -2,2 -1.104,0 -2,-0.896 -2,-2s0.896,-2 2,-2c1.105,0 2,0.896 2,2z" + android:fillColor="#66757F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml new file mode 100644 index 0000000000000000000000000000000000000000..8e3ecc00c0ab4b28e98ee091da1cb8853f529bb0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35,26c0,2.209 -1.791,4 -4,4H5c-2.209,0 -4,-1.791 -4,-4V6.313C1,4.104 6.791,0 9,0h20.625C32.719,0 35,2.312 35,5.375V26z" + android:fillColor="#A0041E"/> + <path + android:pathData="M33,30c0,2.209 -1.791,4 -4,4H7c-2.209,0 -4,-1.791 -4,-4V6c0,-4.119 -0.021,-4 5,-4h21c2.209,0 4,1.791 4,4v24z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M31,31c0,1.657 -1.343,3 -3,3H4c-1.657,0 -3,-1.343 -3,-3V7c0,-1.657 1.343,-3 3,-3h24c1.657,0 3,1.343 3,3v24z" + android:fillColor="#E1E8ED"/> + <path + android:pathData="M31,32c0,2.209 -1.791,4 -4,4H6c-2.209,0 -4,-1.791 -4,-4V10c0,-2.209 1.791,-4 4,-4h21c2.209,0 4,1.791 4,4v22z" + android:fillColor="#BE1931"/> + <path + android:pathData="M29,32c0,2.209 -1.791,4 -4,4H6c-2.209,0 -4,-1.791 -4,-4V12c0,-2.209 1.791,-4 4,-4h19.335C27.544,8 29,9.456 29,11.665V32z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M6,6C4.312,6 4.269,4.078 5,3.25 5.832,2.309 7.125,2 9.438,2H11V0H8.281C4.312,0 1,2.5 1,5.375V32c0,2.209 1.791,4 4,4h2V6H6z" + android:fillColor="#A0041E"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml new file mode 100644 index 0000000000000000000000000000000000000000..d4b557a7edab3ff181338b7535d8d591583acff1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml @@ -0,0 +1,39 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M20.004,20.243c-0.426,0 -0.858,0.01 -1.294,0.031 -0.436,1.268 -0.468,2.747 0,5.097 0.328,1.646 2.659,6.299 4.584,7.933 0.683,0.58 1.638,0.884 2.69,0.884 2.144,0 4.691,-1.265 6.157,-4.034 3.001,-5.671 -3.474,-9.911 -12.137,-9.911z" + android:fillColor="#1C6399"/> + <path + android:pathData="M33.666,1.973c-0.204,0 -0.425,0.021 -0.663,0.066 -3.182,0.601 -9.302,5.126 -14.287,11.771 0,0 -0.789,5.16 -0.789,6.194 0,0.336 1.264,0.5 3.058,0.5 3.717,0 9.709,-0.705 11.424,-2.041 1.898,-1.479 3.65,-9.804 3.488,-14.079 -0.046,-1.175 -0.662,-2.411 -2.231,-2.411z" + android:fillColor="#1C6399"/> + <path + android:pathData="M27.098,13.936l6.629,-0.436s-1.055,3.619 -3.102,4.656 -7.719,1.5 -7.719,1.5 2.33,-4.261 3.286,-5.29c0.237,-0.256 0.559,-0.408 0.906,-0.43zM27.618,11.984l7.526,-8.151s0.002,5.365 -1.206,8.635c0,0 -5.383,0.379 -5.914,0.391 -0.703,0.016 -0.969,-0.265 -0.406,-0.875zM21.55,19.656l5.5,-8.547c0.188,-0.22 0.253,-0.52 0.171,-0.798l-0.968,-3.233 -6.722,6.609 -0.844,6.031 2.863,-0.062zM27.862,8.88c0.172,0.406 0.516,0.5 0.938,0.125s6.074,-6.094 6,-6.218c0,0 -2.832,-1.194 -7.8,3.463 0,0 0.69,2.224 0.862,2.63zM18.937,20.979l5.373,5.228c0.203,0.178 0.255,0.473 0.125,0.709L22.06,31.25s-4.187,-5.479 -3.123,-10.271zM26.219,27.28l5.549,0.741s-1.058,3.845 -3.394,4.854c-3.906,1.688 -5.312,-0.625 -5.312,-0.625l2.352,-4.562c0.151,-0.298 0.477,-0.463 0.805,-0.408zM20.269,20.854l5.375,4.958c0.077,0.066 0.169,0.11 0.269,0.129l6.119,0.903s-1.219,-3.031 -4.429,-4.531c-3.71,-1.733 -7.334,-1.459 -7.334,-1.459z" + android:fillColor="#55ACEE"/> + <path + android:pathData="M20.004,20.243c-0.426,0 -0.858,0.01 -1.294,0.031 -0.436,1.268 -0.468,2.747 0,5.097 0.328,1.646 2.659,6.299 4.584,7.933 0.683,0.58 1.638,0.884 2.69,0.884 2.144,0 4.691,-1.265 6.157,-4.034 3.001,-5.671 -3.474,-9.911 -12.137,-9.911zM30.541,29.569c-1.316,2.486 -3.05,3.473 -4.558,3.473 -0.767,0 -1.704,-0.313 -2.15,-0.691 -1.695,-1.439 -3.437,-4.58 -4.25,-7.224 -0.465,-1.513 -0.354,-4.022 -0.354,-4.022l0.667,-0.021c5.168,0 9.249,2.058 10.726,4.512 0.714,1.186 0.687,2.523 -0.081,3.973z" + android:fillColor="#292F33"/> + <path + android:pathData="M33.666,3.223c0.231,0 0.935,0 0.981,1.208 0.102,2.681 -0.594,6.061 -1.397,8.882 -0.541,1.901 -1.586,3.292 -2.094,3.687 -0.56,0.436 -1.863,1.238 -3.719,1.563 -2.03,0.355 -4.207,0.833 -6.456,0.833 -0.827,0 -1.433,0.019 -1.794,-0.021 0.131,-1.218 0.489,-3.551 0.717,-5.064 3.768,-4.94 9.711,-10.361 13.331,-11.044 0.155,-0.029 0.3,-0.044 0.431,-0.044m0,-1.25c-0.204,0 -0.425,0.021 -0.663,0.066 -3.182,0.601 -9.302,5.126 -14.287,11.771 0,0 -0.789,5.16 -0.789,6.194 0,0.336 1.264,0.5 3.058,0.5 3.717,0 9.709,-0.705 11.424,-2.041 1.898,-1.479 3.65,-9.804 3.488,-14.079 -0.046,-1.175 -0.662,-2.411 -2.231,-2.411z" + android:fillColor="#292F33"/> + <path + android:pathData="M3.902,30.154c1.466,2.769 4.012,4.034 6.157,4.034 1.052,0 2.007,-0.304 2.69,-0.884 1.925,-1.633 4.256,-6.286 4.584,-7.933 0.468,-2.35 0.436,-3.828 0,-5.097 -0.436,-0.021 -0.868,-0.031 -1.294,-0.031 -8.665,0 -15.139,4.24 -12.137,9.911z" + android:fillColor="#1C6399"/> + <path + android:pathData="M2.376,1.973C0.807,1.973 0.19,3.209 0.146,4.383c-0.162,4.275 1.59,12.601 3.488,14.079 1.715,1.336 7.706,2.041 11.424,2.041 1.794,0 3.058,-0.164 3.058,-0.5 0,-1.033 -0.789,-6.194 -0.789,-6.194C12.341,7.165 6.22,2.64 3.039,2.039c-0.238,-0.045 -0.459,-0.066 -0.663,-0.066z" + android:fillColor="#1C6399"/> + <path + android:pathData="M8.943,13.936L2.315,13.5s1.055,3.619 3.102,4.656 7.719,1.5 7.719,1.5 -2.33,-4.261 -3.286,-5.29c-0.237,-0.256 -0.559,-0.408 -0.907,-0.43zM8.424,11.984L0.898,3.833s-0.002,5.365 1.206,8.635c0,0 5.383,0.379 5.914,0.391 0.703,0.016 0.969,-0.265 0.406,-0.875zM14.492,19.656l-5.5,-8.547c-0.188,-0.22 -0.253,-0.52 -0.171,-0.798l0.968,-3.233 6.722,6.609 0.844,6.031 -2.863,-0.062zM8.179,8.88c-0.172,0.406 -0.516,0.5 -0.938,0.125s-6.074,-6.094 -6,-6.218c0,0 2.832,-1.194 7.8,3.463 0.001,0 -0.69,2.224 -0.862,2.63zM17.105,20.979l-5.373,5.228c-0.203,0.178 -0.255,0.473 -0.125,0.709l2.375,4.333c-0.001,0.001 4.187,-5.478 3.123,-10.27zM9.822,27.28l-5.549,0.741s1.058,3.845 3.394,4.854c3.906,1.688 5.312,-0.625 5.312,-0.625l-2.352,-4.562c-0.15,-0.298 -0.476,-0.463 -0.805,-0.408zM15.773,20.854l-5.375,4.958c-0.077,0.066 -0.169,0.11 -0.269,0.129l-6.119,0.903s1.219,-3.031 4.429,-4.531c3.709,-1.733 7.334,-1.459 7.334,-1.459z" + android:fillColor="#55ACEE"/> + <path + android:pathData="M3.902,30.154c1.466,2.769 4.012,4.034 6.157,4.034 1.052,0 2.007,-0.304 2.69,-0.884 1.925,-1.633 4.256,-6.286 4.584,-7.933 0.468,-2.35 0.436,-3.828 0,-5.097 -0.436,-0.021 -0.868,-0.031 -1.294,-0.031 -8.665,0 -15.139,4.24 -12.137,9.911zM5.42,25.595c1.477,-2.454 5.558,-4.512 10.726,-4.512l0.667,0.021s0.111,2.51 -0.354,4.022c-0.813,2.644 -2.555,5.785 -4.25,7.224 -0.446,0.379 -1.383,0.691 -2.15,0.691 -1.508,0 -3.242,-0.986 -4.558,-3.473 -0.768,-1.449 -0.795,-2.786 -0.081,-3.973z" + android:fillColor="#292F33"/> + <path + android:pathData="M2.376,3.223c0.131,0 0.276,0.015 0.431,0.044 3.619,0.683 9.563,6.104 13.331,11.044 0.228,1.513 0.586,3.846 0.717,5.064 -0.361,0.04 -0.967,0.021 -1.794,0.021 -2.249,0 -4.426,-0.478 -6.456,-0.833 -1.856,-0.325 -3.159,-1.127 -3.719,-1.563 -0.508,-0.396 -1.553,-1.786 -2.094,-3.687 -0.803,-2.821 -1.499,-6.201 -1.397,-8.882 0.046,-1.208 0.749,-1.208 0.981,-1.208m0,-1.25C0.807,1.973 0.19,3.209 0.146,4.383c-0.162,4.275 1.59,12.601 3.488,14.079 1.715,1.336 7.706,2.041 11.424,2.041 1.794,0 3.058,-0.164 3.058,-0.5 0,-1.033 -0.789,-6.194 -0.789,-6.194C12.341,7.165 6.22,2.64 3.039,2.039c-0.238,-0.045 -0.459,-0.066 -0.663,-0.066z" + android:fillColor="#292F33"/> + <path + android:pathData="M21.887,4.762c-0.25,-0.138 -0.563,-0.047 -0.701,0.203l-2.74,4.98c-0.018,0.033 -0.022,0.068 -0.032,0.102 -0.127,-0.007 -0.244,-0.018 -0.393,-0.018 -0.148,0 -0.266,0.01 -0.392,0.018 -0.01,-0.034 -0.014,-0.069 -0.032,-0.102l-2.74,-4.98c-0.138,-0.25 -0.452,-0.341 -0.702,-0.203 -0.25,0.137 -0.341,0.451 -0.203,0.701l2.655,4.826c-1.179,0.784 1.15,3.438 0.381,9.204 -1.033,7.75 1.033,9.817 1.033,9.817s2.067,-2.067 1.033,-9.817c-0.769,-5.766 1.56,-8.42 0.381,-9.204l2.656,-4.826c0.137,-0.25 0.046,-0.564 -0.204,-0.701z" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce8aff06575305316fc67341cf808b97d49e4ab0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml @@ -0,0 +1,51 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M30,4c-2.209,0 -4,1.791 -4,4v9.125c0,1.086 -0.887,1.96 -2,2.448V6c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v17.629c-1.122,-0.475 -2,-1.371 -2,-2.504V16c0,-2.209 -1.791,-4 -4,-4s-4,1.791 -4,4v7c0,2.209 1.75,3.875 3.375,4.812 1.244,0.718 4.731,1.6 6.625,1.651V33c0,3.313 12,3.313 12,0v-7.549c1.981,-0.119 5.291,-0.953 6.479,-1.639C32.104,22.875 34,21.209 34,19V8c0,-2.209 -1.791,-4 -4,-4z" + android:fillColor="#77B255"/> + <path + android:pathData="M12,6m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M23,3m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M21,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M14,16m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M20,20m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M13,26m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M5,27m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M9,20m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M2,18m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M34,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M28,11m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M32,16m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M29,24m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> + <path + android:pathData="M22,30m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#3E721D"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml new file mode 100644 index 0000000000000000000000000000000000000000..9ebb3c090483000307bb024eefd346bfa2210338 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml @@ -0,0 +1,42 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M0,26a18,10 0,1 0,36 0a18,10 0,1 0,-36 0z" + android:fillColor="#8899A6"/> + <path + android:pathData="M0,24.25a18,10 0,1 0,36 0a18,10 0,1 0,-36 0z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M32.675,23.685c0,4.26 -6.57,7.712 -14.675,7.712S3.325,27.945 3.325,23.685c0,-4.258 6.57,-7.711 14.675,-7.711 8.104,0 14.675,3.453 14.675,7.711z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M32.233,22.543c0,9.854 -28.466,9.854 -28.466,0v-8.759h28.466v8.759z" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M17.984,18.166c-8.984,0 -14.218,-4.132 -14.218,-4.132s-0.016,0.924 -0.016,1.685c0,0 0.032,4.898 2.572,4.898 2.459,0 2.28,2.348 3.834,2.591 1.541,0.241 1.712,-0.938 3.625,-0.938s2.25,2.106 4.203,2.106c2.289,0 2.477,-2.106 4.389,-2.106s2.132,1.224 3.386,0.885c1.507,-0.408 0.814,-2.537 3.887,-2.537 2.54,0 2.603,-4.648 2.603,-4.648 0,-0.76 -0.017,-1.935 -0.017,-1.935s-5.263,4.131 -14.248,4.131z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M32.675,12.737c0,4.259 -6.57,7.712 -14.675,7.712S3.325,16.996 3.325,12.737 9.895,5.025 18,5.025c8.104,0 14.675,3.453 14.675,7.712z" + android:fillColor="#EA596E"/> + <path + android:pathData="M25.664,13.784c-0.605,0 -1.095,-0.49 -1.095,-1.095V5.025c0,-0.605 0.49,-1.095 1.095,-1.095s1.095,0.49 1.095,1.095v7.664c0,0.605 -0.49,1.095 -1.095,1.095z" + android:fillColor="#FFF8E8"/> + <path + android:pathData="M25.664,6.667c-1.162,0 -2.076,-0.532 -2.445,-1.423 -0.32,-0.773 -0.479,-2.45 2.058,-4.986 0.214,-0.214 0.56,-0.214 0.774,0 2.537,2.537 2.378,4.213 2.058,4.986 -0.369,0.891 -1.283,1.423 -2.445,1.423z" + android:fillColor="#FAAA35"/> + <path + android:pathData="M18,17.068c-0.605,0 -1.095,-0.49 -1.095,-1.095V8.31c0,-0.605 0.49,-1.095 1.095,-1.095s1.095,0.49 1.095,1.095v7.664c0,0.604 -0.49,1.094 -1.095,1.094z" + android:fillColor="#FFF8E8"/> + <path + android:pathData="M18,9.952c-1.162,0 -2.076,-0.532 -2.445,-1.423 -0.321,-0.773 -0.479,-2.45 2.058,-4.986 0.214,-0.214 0.56,-0.214 0.774,0 2.537,2.537 2.378,4.213 2.058,4.986 -0.369,0.891 -1.283,1.423 -2.445,1.423z" + android:fillColor="#FAAA35"/> + <path + android:pathData="M10.336,13.784c-0.605,0 -1.095,-0.49 -1.095,-1.095V5.025c0,-0.605 0.49,-1.095 1.095,-1.095s1.095,0.49 1.095,1.095v7.664c0,0.605 -0.49,1.095 -1.095,1.095z" + android:fillColor="#FFF8E8"/> + <path + android:pathData="M10.336,6.667c-1.162,0 -2.076,-0.532 -2.445,-1.423 -0.321,-0.773 -0.479,-2.45 2.058,-4.986 0.214,-0.214 0.56,-0.214 0.774,0 2.537,2.537 2.378,4.213 2.058,4.986 -0.369,0.891 -1.283,1.423 -2.445,1.423z" + android:fillColor="#FAAA35"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml new file mode 100644 index 0000000000000000000000000000000000000000..b34cf63d98c0a797025b887074c7bfe0157c9bd4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml @@ -0,0 +1,36 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M32.348,13.999s3.445,-8.812 1.651,-11.998c-0.604,-1.073 -8,1.998 -10.723,5.442 0,0 -2.586,-0.86 -5.276,-0.86s-5.276,0.86 -5.276,0.86C10.001,3.999 2.605,0.928 2.001,2.001 0.207,5.187 3.652,13.999 3.652,13.999c-0.897,1.722 -1.233,4.345 -1.555,7.16 -0.354,3.086 0.35,5.546 0.658,6.089 0.35,0.617 2.123,2.605 4.484,4.306 3.587,2.583 8.967,3.445 10.761,3.445s7.174,-0.861 10.761,-3.445c2.361,-1.701 4.134,-3.689 4.484,-4.306 0.308,-0.543 1.012,-3.003 0.659,-6.089 -0.324,-2.814 -0.659,-5.438 -1.556,-7.16z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M2.359,2.971c0.2,-0.599 5.348,2.173 6.518,5.404 0,0 -3.808,2.624 -4.528,4.624 0,0 -2.99,-7.028 -1.99,-10.028z" + android:fillColor="#F18F26"/> + <path + android:pathData="M5.98,7.261c0,-1.414 5.457,2.733 4.457,3.733s-1.255,0.72 -2.255,1.72S5.98,8.261 5.98,7.261z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M33.641,2.971c-0.2,-0.599 -5.348,2.173 -6.518,5.404 0,0 3.808,2.624 4.528,4.624 0,0 2.99,-7.028 1.99,-10.028z" + android:fillColor="#F18F26"/> + <path + android:pathData="M30.02,7.261c0,-1.414 -5.457,2.733 -4.457,3.733s1.255,0.72 2.255,1.72 2.202,-4.453 2.202,-5.453z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M14.001,20.001c0,1.105 -0.896,1.999 -2,1.999s-2,-0.894 -2,-1.999c0,-1.104 0.896,-1.999 2,-1.999s2,0.896 2,1.999zM25.999,20.001c0,1.105 -0.896,1.999 -2,1.999 -1.105,0 -2,-0.894 -2,-1.999 0,-1.104 0.895,-1.999 2,-1.999s2,0.896 2,1.999z" + android:fillColor="#292F33"/> + <path + android:pathData="M2.201,30.458c-0.148,0 -0.294,-0.065 -0.393,-0.19 -0.171,-0.217 -0.134,-0.531 0.083,-0.702 0.162,-0.127 4.02,-3.12 10.648,-2.605 0.275,0.021 0.481,0.261 0.46,0.536 -0.021,0.275 -0.257,0.501 -0.537,0.46 -6.233,-0.474 -9.915,2.366 -9.951,2.395 -0.093,0.07 -0.202,0.106 -0.31,0.106zM11.069,25.795c-0.049,0 -0.1,-0.007 -0.149,-0.022 -4.79,-1.497 -8.737,-0.347 -8.777,-0.336 -0.265,0.081 -0.543,-0.07 -0.623,-0.335 -0.079,-0.265 0.071,-0.543 0.335,-0.622 0.173,-0.052 4.286,-1.247 9.362,0.338 0.264,0.083 0.411,0.363 0.328,0.627 -0.066,0.213 -0.263,0.35 -0.476,0.35zM33.799,30.458c0.148,0 0.294,-0.065 0.393,-0.19 0.171,-0.217 0.134,-0.531 -0.083,-0.702 -0.162,-0.127 -4.02,-3.12 -10.648,-2.605 -0.275,0.021 -0.481,0.261 -0.46,0.536 0.022,0.275 0.257,0.501 0.537,0.46 6.233,-0.474 9.915,2.366 9.951,2.395 0.093,0.07 0.202,0.106 0.31,0.106zM24.931,25.795c0.049,0 0.1,-0.007 0.149,-0.022 4.79,-1.497 8.737,-0.347 8.777,-0.336 0.265,0.081 0.543,-0.07 0.623,-0.335 0.079,-0.265 -0.071,-0.543 -0.335,-0.622 -0.173,-0.052 -4.286,-1.247 -9.362,0.338 -0.264,0.083 -0.411,0.363 -0.328,0.627 0.066,0.213 0.263,0.35 0.476,0.35z" + android:fillColor="#FEE7B8"/> + <path + android:pathData="M24.736,30.898c-0.097,-0.258 -0.384,-0.392 -0.643,-0.294 -0.552,0.206 -1.076,0.311 -1.559,0.311 -1.152,0 -1.561,-0.306 -2.033,-0.659 -0.451,-0.338 -0.956,-0.715 -1.99,-0.803v-2.339c0,-0.276 -0.224,-0.5 -0.5,-0.5s-0.5,0.224 -0.5,0.5v2.373c-0.81,0.115 -1.346,0.439 -1.816,0.743 -0.568,0.367 -1.059,0.685 -2.083,0.685 -0.482,0 -1.006,-0.104 -1.558,-0.311 -0.258,-0.095 -0.547,0.035 -0.643,0.294 -0.097,0.259 0.035,0.547 0.293,0.644 0.664,0.247 1.306,0.373 1.907,0.373 1.319,0 2.014,-0.449 2.627,-0.845 0.524,-0.339 0.98,-0.631 1.848,-0.635 0.992,0.008 1.358,0.278 1.815,0.621 0.538,0.403 1.147,0.859 2.633,0.859 0.601,0 1.244,-0.126 1.908,-0.373 0.259,-0.097 0.391,-0.385 0.294,-0.644z" + android:fillColor="#67757F"/> + <path + android:pathData="M19.4,24.807h-2.8c-0.64,0 -1.163,0.523 -1.163,1.163 0,0.639 0.523,1.163 1.163,1.163h0.237v0.345c0,0.639 0.523,1.163 1.163,1.163s1.163,-0.523 1.163,-1.163v-0.345h0.237c0.639,0 1.163,-0.523 1.163,-1.163s-0.524,-1.163 -1.163,-1.163z" + android:fillColor="#E75A70"/> + <path + android:pathData="M18.022,17.154c-0.276,0 -0.5,-0.224 -0.5,-0.5L17.522,8.37c0,-0.276 0.224,-0.5 0.5,-0.5s0.5,0.224 0.5,0.5v8.284c0,0.277 -0.223,0.5 -0.5,0.5zM21,15.572c-0.276,0 -0.5,-0.224 -0.5,-0.5 0,-2.882 1.232,-5.21 1.285,-5.308 0.13,-0.244 0.435,-0.334 0.677,-0.204 0.243,0.13 0.334,0.433 0.204,0.677 -0.012,0.021 -1.166,2.213 -1.166,4.835 0,0.276 -0.224,0.5 -0.5,0.5zM15,15.572c-0.276,0 -0.5,-0.224 -0.5,-0.5 0,-2.623 -1.155,-4.814 -1.167,-4.835 -0.13,-0.244 -0.038,-0.546 0.205,-0.677 0.242,-0.131 0.545,-0.039 0.676,0.204 0.053,0.098 1.285,2.426 1.285,5.308 0.001,0.276 -0.223,0.5 -0.499,0.5z" + android:fillColor="#F18F26"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml new file mode 100644 index 0000000000000000000000000000000000000000..48d7150c362ee7730299b75f6aeb4670e34497dd --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml @@ -0,0 +1,27 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M20,6.042c0,1.112 -0.903,2.014 -2,2.014s-2,-0.902 -2,-2.014V2.014C16,0.901 16.903,0 18,0s2,0.901 2,2.014v4.028z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M9.18,36c-0.224,0 -0.452,-0.052 -0.666,-0.159 -0.736,-0.374 -1.035,-1.28 -0.667,-2.027l8.94,-18.127c0.252,-0.512 0.768,-0.835 1.333,-0.835s1.081,0.323 1.333,0.835l8.941,18.127c0.368,0.747 0.07,1.653 -0.666,2.027 -0.736,0.372 -1.631,0.07 -1.999,-0.676L18.121,19.74l-7.607,15.425c-0.262,0.529 -0.788,0.835 -1.334,0.835z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M18.121,20.392c-0.263,0 -0.516,-0.106 -0.702,-0.295L3.512,5.998c-0.388,-0.394 -0.388,-1.031 0,-1.424s1.017,-0.393 1.404,0L18.121,17.96 31.324,4.573c0.389,-0.393 1.017,-0.393 1.405,0 0.388,0.394 0.388,1.031 0,1.424l-13.905,14.1c-0.187,0.188 -0.439,0.295 -0.703,0.295z" + android:fillColor="#58595B"/> + <path + android:pathData="M34.015,19.385c0,8.898 -7.115,16.111 -15.894,16.111 -8.777,0 -15.893,-7.213 -15.893,-16.111 0,-8.9 7.116,-16.113 15.893,-16.113 8.778,-0.001 15.894,7.213 15.894,16.113z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M30.041,19.385c0,6.674 -5.335,12.084 -11.92,12.084 -6.583,0 -11.919,-5.41 -11.919,-12.084C6.202,12.71 11.538,7.3 18.121,7.3c6.585,-0.001 11.92,5.41 11.92,12.085z" + android:fillColor="#E6E7E8"/> + <path + android:pathData="M30.04,1.257c-1.646,0 -3.135,0.676 -4.214,1.77l8.429,8.544C35.333,10.478 36,8.968 36,7.299c0,-3.336 -2.669,-6.042 -5.96,-6.042zM5.96,1.257c1.645,0 3.135,0.676 4.214,1.77l-8.429,8.544C0.667,10.478 0,8.968 0,7.299c0,-3.336 2.668,-6.042 5.96,-6.042z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M23,20h-5c-0.552,0 -1,-0.447 -1,-1v-9c0,-0.552 0.448,-1 1,-1s1,0.448 1,1v8h4c0.553,0 1,0.448 1,1 0,0.553 -0.447,1 -1,1z" + android:fillColor="#414042"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml new file mode 100644 index 0000000000000000000000000000000000000000..d390bd6e87a94c040e63d9fc16d2fd1d5d5a5530 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M27,8c-0.701,0 -1.377,0.106 -2.015,0.298 0.005,-0.1 0.015,-0.197 0.015,-0.298 0,-3.313 -2.687,-6 -6,-6 -2.769,0 -5.093,1.878 -5.785,4.427C12.529,6.154 11.783,6 11,6c-3.314,0 -6,2.686 -6,6 0,3.312 2.686,6 6,6 2.769,0 5.093,-1.878 5.785,-4.428 0.686,0.273 1.432,0.428 2.215,0.428 0.375,0 0.74,-0.039 1.096,-0.104 -0.058,0.36 -0.096,0.727 -0.096,1.104 0,3.865 3.135,7 7,7s7,-3.135 7,-7c0,-3.866 -3.135,-7 -7,-7z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M31,22c-0.467,0 -0.91,0.085 -1.339,0.204 0.216,-0.526 0.339,-1.1 0.339,-1.704 0,-2.485 -2.015,-4.5 -4.5,-4.5 -1.019,0 -1.947,0.351 -2.701,0.921C22.093,14.096 19.544,12 16.5,12c-2.838,0 -5.245,1.822 -6.131,4.357C9.621,16.125 8.825,16 8,16c-4.418,0 -8,3.582 -8,8 0,4.419 3.582,8 8,8h23c2.762,0 5,-2.238 5,-5s-2.238,-5 -5,-5z" + android:fillColor="#E1E8ED"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml new file mode 100644 index 0000000000000000000000000000000000000000..d863d03c2ae275e96e323a6bf71d7e7d5fd83364 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M15.373,1.022C13.71,2.686 8.718,9.34 11.214,15.164c2.495,5.823 5.909,2.239 7.486,-2.495 0.832,-2.496 0.832,-5.824 -0.831,-10.815 -0.832,-2.496 -2.496,-0.832 -2.496,-0.832zM34.677,20.326c-1.663,1.663 -8.319,6.655 -14.142,4.159 -5.824,-2.496 -2.241,-5.909 2.495,-7.486 2.497,-0.832 5.823,-0.833 10.814,0.832 2.496,0.831 0.833,2.495 0.833,2.495z" + android:fillColor="#5C913B"/> + <path + android:pathData="M32.314,6.317s-0.145,-1.727 -0.781,-2.253c-0.435,-0.546 -2.018,-0.546 -2.018,-0.546 -1.664,0 -20.798,2.496 -24.125,19.133 -0.595,2.973 4.627,8.241 7.638,7.638C29.667,26.963 32.313,7.98 32.314,6.317z" + android:fillColor="#F4900C"/> + <path + android:pathData="M24.769,8.816l-1.617,-1.617c-0.446,-0.446 -1.172,-0.446 -1.618,0 -0.446,0.447 -0.446,1.171 0,1.617l1.618,1.618c0.445,0.446 1.171,0.446 1.617,0 0.446,-0.446 0.446,-1.17 0,-1.618zM15.064,10.435c0.446,0.446 1.171,0.446 1.617,0 0.447,-0.447 0.447,-1.171 0,-1.618l-0.77,-0.77c-0.654,0.398 -1.302,0.829 -1.938,1.297l1.091,1.091zM17.49,8.008c0.447,0.447 1.17,0.447 1.617,0 0.446,-0.446 0.446,-1.17 0,-1.617l-0.025,-0.025c-0.711,0.325 -1.431,0.688 -2.149,1.086l0.557,0.556zM12.637,12.861c0.447,0.446 1.171,0.446 1.619,0 0.446,-0.447 0.446,-1.171 0,-1.618l-1.198,-1.196c-0.586,0.474 -1.156,0.985 -1.707,1.528l1.286,1.286zM23.96,4.773c-0.447,0.447 -0.447,1.17 0,1.617l1.617,1.617c0.447,0.447 1.171,0.447 1.617,0 0.446,-0.446 0.446,-1.17 0,-1.617l-1.617,-1.617c-0.447,-0.446 -1.17,-0.446 -1.617,0zM26.368,3.977c0.006,0.007 0.008,0.016 0.015,0.023L28,5.617c0.447,0.447 1.171,0.447 1.617,0 0.446,-0.446 0.446,-1.17 0,-1.617l-0.462,-0.462c-0.54,0.044 -1.516,0.172 -2.787,0.439zM22.343,12.861c0.446,-0.447 0.446,-1.171 0,-1.618l-1.618,-1.617c-0.446,-0.447 -1.171,-0.447 -1.617,0 -0.447,0.446 -0.447,1.17 0,1.617l1.617,1.618c0.446,0.446 1.171,0.446 1.618,0zM19.915,15.287c0.447,-0.447 0.447,-1.171 0,-1.618l-1.617,-1.617c-0.446,-0.447 -1.17,-0.447 -1.617,0 -0.446,0.447 -0.446,1.171 0,1.617l1.617,1.618c0.447,0.446 1.172,0.446 1.617,0zM15.064,20.139c0.447,-0.447 0.446,-1.17 0,-1.618l-1.618,-1.617c-0.446,-0.446 -1.169,-0.447 -1.617,0 -0.446,0.447 -0.446,1.171 0,1.617l1.617,1.618c0.447,0.446 1.171,0.446 1.618,0zM14.256,14.478c-0.447,0.446 -0.447,1.171 0,1.618l1.617,1.617c0.447,0.446 1.17,0.446 1.618,0 0.447,-0.447 0.447,-1.171 0,-1.617l-1.618,-1.618c-0.447,-0.447 -1.171,-0.447 -1.617,0z" + android:fillColor="#F7B82D"/> + <path + android:pathData="M27.866,23.574c-7.125,-2.374 -15.097,0.652 -19.418,3.576 2.925,-4.321 5.95,-12.294 3.576,-19.418 -0.934,-2.8 -5.602,-5.601 -8.402,-2.801 -0.934,0.934 -1.867,1.868 0,1.868s4.667,2.8 3.735,5.601c-0.835,2.505 -6.889,8.742 -4.153,15.375 -0.27,0.115 -0.523,0.279 -0.744,0.499l-0.715,0.714c-0.919,0.919 -0.919,2.409 0,3.329l0.716,0.716c0.919,0.92 2.409,0.92 3.328,0l0.715,-0.716c0.123,-0.123 0.227,-0.258 0.316,-0.398 6.999,3.84 13.747,-2.799 16.379,-3.677 2.8,-0.933 5.6,1.868 5.6,3.734 0,1.867 0.934,0.934 1.867,0 2.801,-2.8 -0.001,-7.47 -2.8,-8.402z" + android:fillColor="#77B255"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml new file mode 100644 index 0000000000000000000000000000000000000000..8346a5ebee8abc41602c5e120557bd5b19bdac71 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml @@ -0,0 +1,45 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M15,27v6s0,3 3,3 3,-3 3,-3v-6h-6z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M15,33l0.001,0.037c1.041,-0.035 2.016,-0.274 2.632,-1.286 0.171,-0.281 0.563,-0.281 0.735,0 0.616,1.011 1.591,1.251 2.632,1.286V27h-6v6z" + android:fillColor="#BE1931"/> + <path + android:pathData="M31.954,21.619c0,6.276 -5,6.276 -5,6.276h-18s-5,0 -5,-6.276c0,-6.724 5,-18.619 14,-18.619s14,12.895 14,18.619z" + android:fillColor="#D99E82"/> + <path + android:pathData="M18,20c-7,0 -10,3.527 -10,6.395 0,3.037 2.462,5.5 5.5,5.5 1.605,0 3.042,-0.664 4.049,-2.767 0.185,-0.386 0.716,-0.386 0.901,0 1.007,2.103 2.445,2.767 4.049,2.767 3.038,0 5.5,-2.463 5.5,-5.5C28,23.527 25,20 18,20z" + android:fillColor="#F4C7B5"/> + <path + android:pathData="M15,22.895c-1,1 2,4 3,4s4,-3 3,-4 -5,-1 -6,0zM13,19c-1.1,0 -2,-0.9 -2,-2v-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2v2c0,1.1 -0.9,2 -2,2zM23,19c-1.1,0 -2,-0.9 -2,-2v-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2v2c0,1.1 -0.9,2 -2,2z" + android:fillColor="#292F33"/> + <path + android:pathData="M15,3.608C13.941,2.199 11.681,0.881 2.828,4.2 -1.316,5.754 0.708,17.804 3.935,18.585c1.106,0 4.426,0 4.426,-8.852 0,-0.22 -0.002,-0.423 -0.005,-0.625C10.35,6.298 12.5,4.857 15,3.608zM33.172,4.2C24.319,0.881 22.059,2.199 21,3.608c2.5,1.25 4.65,2.691 6.644,5.501 -0.003,0.201 -0.005,0.404 -0.005,0.625 0,8.852 3.319,8.852 4.426,8.852 3.227,-0.782 5.251,-12.832 1.107,-14.386z" + android:fillColor="#662113"/> + <path + android:pathData="M23.5,25.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M11.5,25.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M25.5,27.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M10.5,27.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M23,28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M13,28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M9.883,7.232c-0.259,-0.673 -0.634,-1.397 -1.176,-1.939 -0.391,-0.391 -1.023,-0.391 -1.414,0s-0.391,1.023 0,1.414c0.57,0.57 1.066,1.934 1.068,2.346 0.145,-0.404 0.839,-1.15 1.522,-1.821zM26.1,7.232c0.259,-0.672 0.634,-1.397 1.176,-1.939 0.391,-0.391 1.023,-0.391 1.414,0s0.391,1.023 0,1.414c-0.57,0.57 -1.066,1.934 -1.068,2.346 -0.145,-0.404 -0.839,-1.15 -1.522,-1.821z" + android:fillColor="#380F09"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0a2de42cbcbd9622797c63433262af576c12433 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M34.453,15.573c-0.864,-7.3 -5.729,-10.447 -13.93,-10.447 -0.391,0 -0.763,0.017 -1.139,0.031 -0.013,-0.01 -0.022,-0.021 -0.035,-0.031C14.655,1.605 4.091,2.779 1.745,6.3c-3.255,4.883 -1.174,22.3 0,24.646 1.173,2.35 4.694,3.521 5.868,2.35 1.174,-1.176 0,-1.176 -1.173,-3.521 -0.85,-1.701 -0.466,-5.859 0.255,-8.471 0.028,0.168 0.068,0.322 0.1,0.486 0.39,2.871 1.993,7.412 1.993,9.744 0,3.564 2.102,4.107 4.694,4.107 2.593,0 4.695,-0.543 4.695,-4.107 0,-0.24 -0.008,-0.463 -0.012,-0.695 0.757,0.064 1.535,0.107 2.359,0.107 0.497,0 0.977,-0.016 1.448,-0.039 -0.004,0.209 -0.013,0.41 -0.013,0.627 0,3.564 2.103,4.107 4.694,4.107 2.593,0 4.695,-0.543 4.695,-4.107 0,-1.801 1.192,-4.625 2.039,-6.982 0.159,-0.354 0.291,-0.732 0.42,-1.117 0.118,1.307 0.193,2.706 0.193,4.206 0,0.553 0.447,1 1,1s1,-0.447 1,-1c0,-5.153 -0.771,-9.248 -1.547,-12.068z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M19.35,5.126S23,10.641 20,15.641c-3,5 -7.838,5 -11,5 -2,0 -1,2 0,2 1.414,0 8.395,1.211 12,-6 3,-6 -1.65,-11.515 -1.65,-11.515z" + android:fillColor="#66757F"/> + <path + android:pathData="M6.5,14.141m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml new file mode 100644 index 0000000000000000000000000000000000000000..ebf42039b1dbdc20964ddcba939224b4d0e46879 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35,19c0,-2.062 -0.367,-4.039 -1.04,-5.868 -0.46,5.389 -3.333,8.157 -6.335,6.868 -2.812,-1.208 -0.917,-5.917 -0.777,-8.164 0.236,-3.809 -0.012,-8.169 -6.931,-11.794 2.875,5.5 0.333,8.917 -2.333,9.125 -2.958,0.231 -5.667,-2.542 -4.667,-7.042 -3.238,2.386 -3.332,6.402 -2.333,9 1.042,2.708 -0.042,4.958 -2.583,5.208 -2.84,0.28 -4.418,-3.041 -2.963,-8.333C2.52,10.965 1,14.805 1,19c0,9.389 7.611,17 17,17s17,-7.611 17,-17z" + android:fillColor="#F4900C"/> + <path + android:pathData="M28.394,23.999c0.148,3.084 -2.561,4.293 -4.019,3.709 -2.106,-0.843 -1.541,-2.291 -2.083,-5.291s-2.625,-5.083 -5.708,-6c2.25,6.333 -1.247,8.667 -3.08,9.084 -1.872,0.426 -3.753,-0.001 -3.968,-4.007C7.352,23.668 6,26.676 6,30c0,0.368 0.023,0.73 0.055,1.09C9.125,34.124 13.342,36 18,36s8.875,-1.876 11.945,-4.91c0.032,-0.36 0.055,-0.722 0.055,-1.09 0,-2.187 -0.584,-4.236 -1.606,-6.001z" + android:fillColor="#FFCC4D"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml new file mode 100644 index 0000000000000000000000000000000000000000..30907f2496ad37b32f774463e61aa2ba4ebe958b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M32.153,24c0,-1 1.523,-6.212 3.047,-7.735 1.522,-1.523 0,-3.166 -1.523,-3.166 -3.405,0 -9.139,6.901 -9.139,10.901 0,5 5.733,10.424 9.139,10.424 1.523,0 3.046,-1.404 1.523,-2.928C33.677,29.974 32.153,26 32.153,24z" + android:fillColor="#3B88C3"/> + <path + android:pathData="M9.021,14.384c0,-3.046 1.497,-6.093 3.02,-6.093 4.569,0 13.322,4.823 14.845,12.439 1.524,7.616 -17.865,-6.346 -17.865,-6.346zM13.875,32.662c1.523,1.523 4.57,3.047 7.617,3.047 3.046,0 -3.111,-4.189 -1.523,-6.092 2.18,-2.617 -6.094,3.045 -6.094,3.045z" + android:fillColor="#3B88C3"/> + <path + android:fillColor="#FF000000" + android:pathData="M2.071,28.727c0.761,-2.285 0.19,-3.935 -1.143,-5.584 -1.333,-1.651 3.872,-1.904 5.585,0.381s5.713,6.281 2.158,6.22c-3.553,-0.065 -6.6,-1.017 -6.6,-1.017z"/> + <path + android:pathData="M0.168,23.488c0.959,0.874 7.223,4.309 7.165,5.137 -0.058,0.828 -2.279,-0.088 -3.105,-0.279 -1.485,-0.342 -1.905,-0.598 -2.317,-0.526 -0.84,0.321 -0.554,1.201 -0.242,1.704 1.498,2.61 7.286,4.662 12.16,4.662 8.412,0 16.802,-7.615 16.802,-10.662 0,-3.046 -9.345,-10.663 -17.757,-10.663C4.483,12.86 0.18,18.922 0.168,23.488z" + android:fillColor="#55ACEE"/> + <path + android:fillColor="#FF000000" + android:pathData="M7,17c1.104,0 2,0.894 2,2 0,1.105 -0.896,2 -2,2 -1.105,0 -2,-0.896 -2,-2 0,-1.106 0.895,-2 2,-2z"/> + <path + android:pathData="M15.08,29.98c-0.156,0 -0.314,-0.036 -0.462,-0.113 -0.49,-0.256 -0.679,-0.86 -0.423,-1.35 1.585,-3.034 2.218,-5.768 0.154,-9.243 -0.282,-0.475 -0.126,-1.088 0.349,-1.371 0.475,-0.283 1.088,-0.124 1.371,0.349 2.693,4.535 1.46,8.202 -0.102,11.191 -0.178,0.342 -0.527,0.537 -0.887,0.537z" + android:fillColor="#269"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml new file mode 100644 index 0000000000000000000000000000000000000000..250388dc4a946260eb9781878b25971dabae0f46 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M5,36c-1.104,0 -2,-0.896 -2,-2V3c0,-1.104 0.896,-2 2,-2s2,0.896 2,2v31c0,1.104 -0.896,2 -2,2z" + android:fillColor="#8899A6"/> + <path + android:pathData="M5,1c-1.105,0 -2,0.895 -2,2v31c0,0.276 0.224,0.5 0.5,0.5s0.5,-0.224 0.5,-0.5V4.414C4,3.633 4.633,3 5.414,3H7c0,-1.105 -0.895,-2 -2,-2z" + android:fillColor="#AAB8C2"/> + <path + android:pathData="M5,36c-1.104,0 -2,-0.896 -2,-2V3c0,-1.104 0.896,-2 2,-2s2,0.896 2,2v31c0,1.104 -0.896,2 -2,2z" + android:fillColor="#8899A6"/> + <path + android:pathData="M5,1c-1.105,0 -2,0.895 -2,2v31c0,0.276 0.224,0.5 0.5,0.5s0.5,-0.224 0.5,-0.5V4.414C4,3.633 4.633,3 5.414,3H7c0,-1.105 -0.895,-2 -2,-2z" + android:fillColor="#AAB8C2"/> + <path + android:pathData="M32.396,3.082C30.732,2.363 28.959,2.006 27,1.974l-1.375,0.38L21,3l-1,-0.128c-0.237,0.051 -0.476,0.099 -0.711,0.15 -2.169,0.469 -4.23,0.894 -6.289,0.982L12,5 6,4v19h6l1,2h0.077c2.244,-0.096 4.472,-0.556 6.633,-1.022l0.29,-0.061 0.646,-0.645 5.438,-0.708 0.916,0.41c1.68,0.032 3.193,0.335 4.604,0.944 0.309,0.133 0.665,0.103 0.945,-0.082 0.282,-0.186 0.451,-0.499 0.451,-0.836V4c0,-0.399 -0.237,-0.76 -0.604,-0.918z" + android:fillColor="#31373D"/> + <path + android:pathData="M13,4.004c-0.239,0.01 -0.478,0.035 -0.717,0.035 -1.797,0 -3.396,-0.313 -4.887,-0.957 -0.308,-0.135 -0.665,-0.103 -0.945,0.083C6.169,3.349 6,3.664 6,4v6s3.292,1 7,1L13,4.004zM20,10s-3.75,1 -7,1v7c3,0 7,-1 7,-1v-7zM27,9L27,1.974c-0.096,-0.002 -0.186,-0.013 -0.283,-0.013 -2.267,0 -4.521,0.442 -6.717,0.911L20,10s2.167,-1 7,-1zM6.604,23.918c1.5,0.648 3.09,0.993 4.82,1.082L13,25v-7c-4.167,0 -7,-1 -7,-1v6c0,0.399 0.237,0.76 0.604,0.918zM20,17v6.916c2.313,-0.499 4.511,-0.955 6.717,-0.955 0.097,0 0.187,0.011 0.283,0.013L27,16c-4.5,0 -7,1 -7,1zM27,16c2.676,0 4.82,0.56 6,0.954L33,9.908C31.853,9.527 29.769,9 27,9v7z" + android:fillColor="#E1E8ED"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml new file mode 100644 index 0000000000000000000000000000000000000000..8a91221a8033514c3809912edf5d934e7c27c320 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M34.751,22c-3.382,0 -11.9,3.549 -15.751,7.158V17c0,-0.553 -0.447,-1 -1,-1 -0.552,0 -1,0.447 -1,1v12.341C13.247,25.669 4.491,22 1.052,22 0.123,22 11.913,35.992 17,34.599V35c0,0.553 0.448,1 1,1 0.553,0 1,-0.447 1,-1v-0.356C24.188,35.638 35.668,22 34.751,22z" + android:fillColor="#77B255"/> + <path + android:pathData="M25,13.417C25,19.768 23.293,23 18,23s-7,-3.232 -7,-9.583S16,0 18,0s7,7.066 7,13.417z" + android:fillColor="#EA596E"/> + <path + android:pathData="M22.795,2c-0.48,0 -4.106,14.271 -4.803,19.279C17.246,16.271 13.481,2 13,2c-1,0 -6,9 -6,13s5.707,8 11,8 10.795,-4 10.795,-8 -5,-13 -6,-13z" + android:fillColor="#F4ABBA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml new file mode 100644 index 0000000000000000000000000000000000000000..9320766492cde6420ef8a9ae7d651d6063d2ddb6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M0,29c0,2.209 1.791,4 4,4h24c2.209,0 4,-1.791 4,-4V12c0,-2.209 -1.791,-4 -4,-4h-9c-3.562,0 -3,-5 -8.438,-5H4C1.791,3 0,4.791 0,7v22z" + android:fillColor="#269"/> + <path + android:pathData="M30,10h-6.562C18,10 18.562,15 15,15H6c-2.209,0 -4,1.791 -4,4v10c0,0.553 -0.448,1 -1,1s-1,-0.447 -1,-1c0,2.209 1.791,4 4,4h26c2.209,0 4,-1.791 4,-4V14c0,-2.209 -1.791,-4 -4,-4z" + android:fillColor="#55ACEE"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml new file mode 100644 index 0000000000000000000000000000000000000000..d18c6e860a801e2180bb3abed89d4231ce60e4b6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M33,31c0,2.2 -1.8,4 -4,4H7c-2.2,0 -4,-1.8 -4,-4V14c0,-2.2 1.8,-4 4,-4h22c2.2,0 4,1.8 4,4v17z" + android:fillColor="#FDD888"/> + <path + android:pathData="M36,11c0,2.2 -1.8,4 -4,4H4c-2.2,0 -4,-1.8 -4,-4s1.8,-4 4,-4h28c2.2,0 4,1.8 4,4z" + android:fillColor="#FDD888"/> + <path + android:pathData="M3,15h30v2H3z" + android:fillColor="#FCAB40"/> + <path + android:pathData="M19,3h-2c-1.657,0 -3,1.343 -3,3v29h8V6c0,-1.656 -1.343,-3 -3,-3z" + android:fillColor="#DA2F47"/> + <path + android:pathData="M16,7c1.1,0 1.263,-0.516 0.361,-1.147L9.639,1.147c-0.902,-0.631 -2.085,-0.366 -2.631,0.589L4.992,5.264C4.446,6.219 4.9,7 6,7h10zM20,7c-1.1,0 -1.263,-0.516 -0.361,-1.147l6.723,-4.706c0.901,-0.631 2.085,-0.366 2.631,0.589l2.016,3.527C31.554,6.219 31.1,7 30,7L20,7z" + android:fillColor="#DA2F47"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml new file mode 100644 index 0000000000000000000000000000000000000000..8913d1ffd7edd840d6df50b8bd90f96c7b7a80f2 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35.686,11.931c-0.507,-0.522 -6.83,-1.094 -13.263,-0.369 -1.283,0.144 -1.363,0.51 -4.425,0.63 -3.061,-0.119 -3.141,-0.485 -4.425,-0.63C7.14,10.837 0.817,11.41 0.31,11.931c-0.252,0.261 -0.252,2.077 0,2.338 0.254,0.261 1.035,0.606 1.403,1.827 0.237,0.787 0.495,5.864 2.281,7.377 1.768,1.498 7.462,1.217 9.326,0.262 2.536,-1.298 2.892,-5.785 3.292,-7.639 0.203,-0.939 1.162,-1.016 1.385,-1.016s1.182,0.077 1.385,1.016c0.401,1.853 0.757,6.34 3.292,7.639 1.865,0.955 7.558,1.236 9.326,-0.262 1.786,-1.513 2.044,-6.59 2.281,-7.377 0.368,-1.22 1.149,-1.566 1.403,-1.827 0.254,-0.26 0.254,-2.077 0.002,-2.338z" + android:fillColor="#31373D"/> + <path + android:pathData="M14.644,15.699c-0.098,1.255 -0.521,4.966 -1.757,6.083 -1.376,1.243 -6.25,1.568 -7.79,0.044 -0.808,-0.799 -1.567,-4.018 -1.503,-6.816 0.038,-1.679 2.274,-2.02 5.462,-2.02 3.148,0 5.763,0.468 5.588,2.709zM21.351,15.699c0.098,1.255 0.521,4.966 1.757,6.083 1.376,1.243 6.25,1.568 7.79,0.044 0.808,-0.799 1.567,-4.018 1.503,-6.816 -0.038,-1.679 -2.274,-2.02 -5.462,-2.02 -3.147,0 -5.763,0.468 -5.588,2.709z" + android:fillColor="#55ACEE"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml new file mode 100644 index 0000000000000000000000000000000000000000..2a07829cb3d7ad5de4eac1f9f4cc936bf503e513 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="#88C9F9"/> + <path + android:pathData="M2.812,25.375c-0.062,-1 -0.062,-1.187 -0.062,-2.375s0.562,-1 1.125,-1.562 0.438,-0.625 1.375,-1.241 0.438,-1.321 0.375,-1.696 -0.625,-0.063 -1.563,0.061 -0.624,-0.312 -1.187,-0.562 -0.812,-0.625 -1.188,-1.75 -0.438,-1.438 -0.312,-2.375 0.563,-0.063 0.625,0.937 0.938,0.625 0.938,1.25 1.25,1.312 1.562,1.5 1.188,-0.938 1.5,-1.25 0.688,-0.75 0.812,-1 1.688,-0.438 2,-0.438 1.062,0.938 1.062,1.375 0.375,1.625 0.688,2.312 1,0.812 1.625,1.312 0.938,0.062 0.938,0.062 -0.25,-1.062 -0.25,-1.938 0.75,-1.625 0.75,-1.625 1.188,0.875 1.25,1.125 1,1.125 1.062,1.562 0.562,1 1.483,1.125 0.267,-1.062 0.579,-1.875 0.75,-0.938 1.312,-1.062 1,-0.625 1.375,-1.125 1.062,-1.188 1,-1.75 -0.25,-0.938 -0.5,-1.625 0.75,-0.938 1.188,-1.75 0,0 1,-0.25 0.562,-0.25 0.75,-0.625 0.312,-0.75 0.125,-1.438 -0.875,0 -1.562,0S22.938,7.75 23,7s0.938,-0.562 1.562,-0.625 0.812,0.812 1,1 2.125,-1.25 2.625,-1.938 -0.437,-0.499 -0.187,-0.789 -1.5,-0.349 -2.188,-0.46 -2.437,-0.188 -3.124,-0.612 -3.312,-0.104 -4,0.237 -1.125,-0.029 -1.438,-0.5 -1.625,-0.235 -2,-0.5 -0.75,0.437 -1.25,0.625 -0.688,0.25 -1.312,-0.125 0.187,-0.813 -0.688,-1.125c-0.586,-0.209 -1.288,-0.087 -2.38,-0.111C3.902,5.092 0,11.087 0,18c0,3.42 0.971,6.605 2.627,9.327 0.308,-0.518 0.231,-1.217 0.185,-1.952zM17.312,24.188c0.438,0.062 1.688,0 0.688,-0.812s-1.562,-0.188 -1.438,-1.125 -0.625,-0.938 -0.625,-0.938c0,0.688 -0.5,1.438 0,2.125s0.938,0.687 1.375,0.75z" + android:fillColor="#5C913B"/> + <path + android:pathData="M23.688,13.75c-1,-0.812 -0.25,-0.562 -0.125,-1.5s-0.625,-0.938 -0.625,-0.938c0,0.688 -0.5,1.438 0,2.125s-1,1.25 -0.562,1.312 2.312,-0.187 1.312,-0.999zM19.808,23.5c0.62,0.688 0.38,0 1.192,-0.312s-0.688,-1 -1.188,-1.375 -0.997,-0.389 -1.434,0.438c-0.496,0.937 0.81,0.561 1.43,1.249zM27.125,24.75c-0.312,-0.375 -1,-0.562 -1.75,-0.545 -0.75,0.018 -0.688,-0.83 -1.438,-0.768s-1.286,-0.504 -1.625,-0.679c-0.737,-0.38 -0.25,0.491 0,1.446s1.188,0.232 2.062,0.732 0.938,-0.188 1.75,0.062 1.125,0.812 1.904,0.75 -0.59,-0.623 -0.903,-0.998zM25.5,27.5c-0.312,-0.625 -1.226,-1.188 -1.601,-1.505s-0.962,-0.424 -1.462,-0.24 -0.812,0 -1.062,-0.495 -0.688,-0.322 -1.062,-0.26 -1.875,0.688 -2.75,1.125 -1.273,0.817 -1.847,1.375c-0.898,0.874 -0.403,0.312 0,0.875 0.403,0.562 -0.442,2.312 -0.504,3.312s1.602,-0.312 2.227,-0.438 0.441,-0.5 0.941,-0.875 0.825,-0.463 1.374,0.037c0.549,0.5 1.268,0.963 1.268,1.525s1.979,1.5 2.729,1.125 1.188,-1.125 1.875,-1.75 0.438,-1.812 0.625,-2.562 -0.439,-0.624 -0.751,-1.249z" + android:fillColor="#5C913B"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml new file mode 100644 index 0000000000000000000000000000000000000000..2622fbe416dfe4a68d56e75020268015bf85c3f3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml @@ -0,0 +1,51 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M21.828,20.559C19.707,21.266 19,17.731 19,17.731s0.965,-0.968 0.235,-1.829c1.138,-1.137 0.473,-1.707 0.473,-1.707 -1.954,-1.953 -5.119,-1.953 -7.071,0 -0.246,0.246 -0.414,0.467 -0.553,0.678 -0.061,0.086 -0.115,0.174 -0.17,0.262l-0.014,0.027c-0.285,0.475 -0.491,0.982 -0.605,1.509 -0.156,0.319 -0.379,0.659 -0.779,1.06 -1.414,1.414 -4.949,-0.707 -7.778,2.121 -0.029,0.029 -0.045,0.069 -0.069,0.104 -0.094,0.084 -0.193,0.158 -0.284,0.25 -3.319,3.319 -3.003,9.018 0.708,12.728 3.524,3.525 8.84,3.979 12.209,1.17 0.058,-0.031 0.117,-0.061 0.165,-0.109 0.071,-0.072 0.126,-0.14 0.193,-0.21 0.053,-0.049 0.109,-0.093 0.161,-0.143 1.693,-1.694 2.342,-3.73 2.086,-5.811 -0.068,-0.99 -0.165,-1.766 0.39,-2.321 0.707,-0.707 2.828,0 4.242,-1.414 2.117,-2.122 0.631,-3.983 -0.711,-3.537z" + android:fillColor="#BB1A34"/> + <path + android:pathData="M14.987,18.91L30.326,3.572l2.121,2.122 -15.339,15.339z" + android:fillColor="#292F33"/> + <path + android:pathData="M10.001,29.134c1.782,1.277 1.959,3.473 1.859,4.751 -0.042,0.528 0.519,0.898 0.979,0.637 2.563,-1.456 4.602,-3.789 4.038,-7.853 -0.111,-0.735 0.111,-2.117 2.272,-2.406 2.161,-0.29 2.941,-1.099 3.208,-1.485 0.153,-0.221 0.29,-0.832 -0.312,-0.854 -0.601,-0.022 -2.094,0.446 -3.431,-1.136 -1.337,-1.582 -1.559,-2.228 -1.604,-2.473 -0.045,-0.245 -1.409,-3.694 -2.525,-1.864 -0.927,1.521 -1.958,4.509 -5.287,5.287 -1.355,0.316 -3.069,1.005 -3.564,1.96 -0.832,1.604 0.46,2.725 1.574,3.483 1.115,0.757 2.793,1.953 2.793,1.953z" + android:fillColor="#F5F8FA"/> + <path + android:pathData="M13.072,19.412l1.414,-1.415 3.536,3.535 -1.414,1.414zM8.597,23.886l1.415,-1.414 3.535,3.535 -1.414,1.414z" + android:fillColor="#292F33"/> + <path + android:pathData="M7.396,27.189L29.198,5.427l0.53,0.531L7.927,27.72zM8.265,28.057L30.067,6.296l0.53,0.531L8.796,28.59z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M9.815,28.325c0.389,0.389 0.389,1.025 0,1.414s-1.025,0.389 -1.414,0l-2.122,-2.121c-0.389,-0.389 -0.389,-1.025 0,-1.414h0.001c0.389,-0.389 1.025,-0.389 1.414,0l2.121,2.121z" + android:fillColor="#292F33"/> + <path + android:pathData="M13.028,29.556m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#292F33"/> + <path + android:pathData="M14.445,31.881c0,0.379 -0.307,0.686 -0.686,0.686 -0.379,0 -0.686,-0.307 -0.686,-0.686 0,-0.379 0.307,-0.686 0.686,-0.686 0.379,0 0.686,0.307 0.686,0.686z" + android:fillColor="#292F33"/> + <path + android:pathData="M35.088,4.54c0.415,0.415 0.415,1.095 -0.001,1.51l-4.362,3.02c-0.416,0.415 -1.095,0.415 -1.51,0L26.95,6.804c-0.415,-0.415 -0.415,-1.095 0.001,-1.51l3.02,-4.361c0.416,-0.415 1.095,-0.415 1.51,0l3.607,3.607z" + android:fillColor="#BB1A34"/> + <path + android:pathData="M32.123,9.402m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> + <path + android:pathData="M33.381,8.557m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> + <path + android:pathData="M34.64,7.712m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> + <path + android:pathData="M26.712,3.811m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> + <path + android:pathData="M27.555,2.571m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> + <path + android:pathData="M28.398,1.332m-0.625,0a0.625,0.625 0,1 1,1.25 0a0.625,0.625 0,1 1,-1.25 0" + android:fillColor="#66757F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml new file mode 100644 index 0000000000000000000000000000000000000000..7b70654d52b89729bfb5589fd90a80dc14c9606e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M29.879,33.879C31.045,35.045 32.9,35.1 34,34s1.045,-2.955 -0.121,-4.121L12.121,8.121C10.955,6.955 9.1,6.9 8,8s-1.045,2.955 0.121,4.121l21.758,21.758z" + android:fillColor="#F4900C"/> + <path + android:pathData="M22,3s-6,-3 -11,2l-7,7s-1,-1 -2,0l-1,1s-1,1 0,2l4,4s1,1 2,0l1,-1s1,-1 0,-2l-0.078,-0.078c0.77,-0.743 1.923,-1.5 3.078,-0.922l4,-4s-1,-3 1,-5 3,-2 5,-2 1,-1 1,-1z" + android:fillColor="#66757F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml new file mode 100644 index 0000000000000000000000000000000000000000..15f980bdb1f6db738aad41269962cdbc1a716228 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M30.198,27.385L32,3.816c0,-0.135 -0.008,-0.263 -0.021,-0.373 0.003,-0.033 0.021,-0.075 0.021,-0.11C32,1.529 25.731,0.066 18,0.066c-7.732,0 -14,1.462 -14,3.267 0,0.035 0.017,0.068 0.022,0.102 -0.014,0.11 -0.022,0.23 -0.022,0.365l1.802,23.585C2.298,28.295 0,29.576 0,31c0,2.762 8.611,5 18,5s18,-2.238 18,-5c0,-1.424 -2.298,-2.705 -5.802,-3.615z" + android:fillColor="#31373D"/> + <path + android:pathData="M17.536,6.595c-4.89,0 -8.602,-0.896 -10.852,-1.646 -0.524,-0.175 -0.808,-0.741 -0.633,-1.265 0.175,-0.524 0.739,-0.808 1.265,-0.633 2.889,0.963 10.762,2.891 21.421,-0.016 0.529,-0.142 1.082,0.168 1.227,0.702 0.146,0.533 -0.169,1.083 -0.702,1.228 -4.406,1.202 -8.347,1.63 -11.726,1.63z" + android:fillColor="#66757F"/> + <path + android:pathData="M30.198,27.385l0.446,-5.829c-7.705,2.157 -17.585,2.207 -25.316,-0.377l0.393,5.142c0.069,0.304 0.113,0.65 0.113,1.076 0,1.75 1.289,2.828 2.771,3.396 4.458,1.708 13.958,1.646 18.807,0.149 1.467,-0.453 2.776,-1.733 2.776,-3.191 0,-0.119 0.015,-0.241 0.024,-0.361l-0.014,-0.005z" + android:fillColor="#744EAA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml new file mode 100644 index 0000000000000000000000000000000000000000..cbc43e76010421f13cd0b0b4296fc4ecff8d3310 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,0C9.716,0 3,6.716 3,15v9h3v-9C6,8 11.269,2.812 18,2.812 24.73,2.812 30,8 30,15v10l3,-1v-9c0,-8.284 -6.716,-15 -15,-15z" + android:fillColor="#66757F"/> + <path + android:pathData="M6,27c0,1.104 -0.896,2 -2,2L2,29c-1.104,0 -2,-0.896 -2,-2v-9c0,-1.104 0.896,-2 2,-2h2c1.104,0 2,0.896 2,2v9zM36,27c0,1.104 -0.896,2 -2,2h-2c-1.104,0 -2,-0.896 -2,-2v-9c0,-1.104 0.896,-2 2,-2h2c1.104,0 2,0.896 2,2v9z" + android:fillColor="#31373D"/> + <path + android:pathData="M19.182,10.016l-6.364,1.313c-0.45,0.093 -0.818,0.544 -0.818,1.004v16.185c-0.638,-0.227 -1.341,-0.36 -2.087,-0.36 -2.785,0 -5.042,1.755 -5.042,3.922 0,2.165 2.258,3.827 5.042,3.827C12.649,35.905 14.922,34 15,32L15,16.39l4.204,-0.872c0.449,-0.093 0.796,-0.545 0.796,-1.004v-3.832c0,-0.458 -0.368,-0.759 -0.818,-0.666zM27.182,13.167l-4.297,0.865c-0.45,0.093 -0.885,0.544 -0.885,1.003L22,26.44c0,-0.152 -0.878,-0.24 -1.4,-0.24 -2.024,0 -3.633,1.276 -3.633,2.852 0,1.574 1.658,2.851 3.683,2.851s3.677,-1.277 3.677,-2.851l-0.014,-11.286 2.869,-0.598c0.45,-0.093 0.818,-0.544 0.818,-1.003v-2.33c0,-0.459 -0.368,-0.76 -0.818,-0.668z" + android:fillColor="#55ACEE"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml new file mode 100644 index 0000000000000000000000000000000000000000..d37bcc33d1707679b27902ab0fea71dcb9a7b1ae --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35.885,11.833c0,-5.45 -4.418,-9.868 -9.867,-9.868 -3.308,0 -6.227,1.633 -8.018,4.129 -1.791,-2.496 -4.71,-4.129 -8.017,-4.129 -5.45,0 -9.868,4.417 -9.868,9.868 0,0.772 0.098,1.52 0.266,2.241C1.751,22.587 11.216,31.568 18,34.034c6.783,-2.466 16.249,-11.447 17.617,-19.959 0.17,-0.721 0.268,-1.469 0.268,-2.242z" + android:fillColor="#DD2E44"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml new file mode 100644 index 0000000000000000000000000000000000000000..bedf0f6f466f32a6dc7bf56ed9dcd5c2ccaec312 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml @@ -0,0 +1,33 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M28.721,12.849s3.809,1.643 5.532,0.449c1.723,-1.193 2.11,-2.773 1.159,-4.736 -0.951,-1.961 -3.623,-2.732 -3.712,-5.292 0,0 -0.298,4.141 1.513,5.505 2.562,1.933 -0.446,4.21 -3.522,3.828 -3.078,-0.382 -0.97,0.246 -0.97,0.246z" + android:fillColor="#292F33"/> + <path + android:pathData="M23.875,19.375s-0.628,2.542 0.187,5.03c0.145,0.341 0.049,0.556 -0.208,0.678 -0.256,0.122 -4.294,1.542 -4.729,1.771 -0.396,0.208 -1.142,1.78 -1.208,2.854 0.844,0.218 1.625,0.104 1.625,0.104s0.025,-1.915 0.208,-2.042c0.183,-0.127 5.686,-1.048 6.062,-1.771s1.611,-3.888 0.812,-5.292c-0.225,-0.395 -0.637,-1.15 -0.637,-1.15l-2.112,-0.182z" + android:fillColor="#8A4B38"/> + <path + android:pathData="M17.917,29.708s-0.616,1.993 0.008,2.138c0.605,0.141 1.694,-0.388 1.755,-0.646 0.081,-0.343 0.216,-1.179 0.098,-1.366 -0.118,-0.186 -1.861,-0.126 -1.861,-0.126z" + android:fillColor="#292F33"/> + <path + android:pathData="M11.812,21.875l-0.75,-2.562s-2.766,2.105 -3.938,3.594c-0.344,0.437 -1.847,3.198 -1.722,4.413 0.05,0.488 0.474,2.583 0.474,2.583l1.651,-0.465s-1.312,-1.896 -1.021,-2.562c1.428,-3.263 5.306,-5.001 5.306,-5.001z" + android:fillColor="#8A4B38"/> + <path + android:pathData="M7.679,29.424c-0.172,-0.139 -1.803,0.479 -1.803,0.479s0.057,2.085 0.695,2.022c0.618,-0.061 1.48,-0.912 1.455,-1.175 -0.034,-0.351 -0.175,-1.187 -0.347,-1.326z" + android:fillColor="#292F33"/> + <path + android:pathData="M27.188,11.188c-3.437,0.156 -7.207,0.438 -9.5,0.438 -3.655,0 -5.219,-1.428 -6.562,-2.625C8.838,6.964 8.167,4.779 6,5.501c0,0 -0.632,-0.411 -1.247,-0.778l-0.261,-0.152c-0.256,-0.148 -0.492,-0.276 -0.656,-0.347 -0.164,-0.072 -0.258,-0.087 -0.228,-0.01 0.019,0.051 0.093,0.143 0.236,0.286 0.472,0.472 0.675,0.95 0.728,1.395 -2.01,1.202 -2.093,2.276 -2.871,3.552 -0.492,0.807 -1.36,2.054 -1.56,2.515 -0.412,0.948 1.024,2.052 1.706,1.407 0.893,-0.845 0.961,-1.122 2.032,-1.744 0.983,-0.016 1.975,-0.416 2.308,-1.02 0,0 0.938,2.083 1.938,3.583s2.5,3.125 2.5,3.125c-0.131,1.227 0.12,2.176 0.549,2.922 -0.385,0.757 -0.924,1.807 -1.417,2.745 -0.656,1.245 -1.473,3.224 -1.208,3.618 0.534,0.798 2.719,2.926 4.137,3.311 1.03,0.28 2.14,0.437 2.14,0.437l-0.193,-1.574s-1.343,0.213 -1.875,-0.083c-1.427,-0.795 -2.666,-2.248 -2.708,-2.542 -0.07,-0.487 3.841,-2.868 5.14,-3.645 2.266,0.097 6.022,-0.369 8.626,-1.702 0.958,1.86 2.978,2.513 2.978,2.513s0.667,2.208 1.375,4.125c-1.017,0.533 -4.468,3.254 -4.975,3.854 -0.456,0.54 -0.856,2.49 -0.856,2.49 0.82,0.375 1.57,0.187 1.57,0.187s0.039,-1.562 0.385,-2.073c0.346,-0.511 4.701,-2.559 5.958,-3.458 0.492,-0.352 0.404,-0.903 0.262,-1.552 -0.321,-1.471 -0.97,-4.781 -0.971,-4.782 5.146,-2.979 6.458,-11.316 -2.354,-10.916z" + android:fillColor="#C1694F"/> + <path + android:pathData="M22.336,33.782s-0.616,1.993 0.008,2.138c0.605,0.141 1.694,-0.388 1.755,-0.646 0.081,-0.343 0.216,-1.179 0.098,-1.366 -0.118,-0.187 -1.861,-0.126 -1.861,-0.126zM14.66,28.486c-0.167,0.146 0.164,1.859 0.164,1.859s2.064,0.299 2.111,-0.34c0.045,-0.62 -0.647,-1.614 -0.91,-1.634 -0.351,-0.027 -1.198,-0.031 -1.365,0.115z" + android:fillColor="#292F33"/> + <path + android:pathData="M4.25,8.047m-0.349,0a0.349,0.349 0,1 1,0.698 0a0.349,0.349 0,1 1,-0.698 0" + android:fillColor="#292F33"/> + <path + android:pathData="M12.655,9.07c1.773,1.446 3.147,0.322 3.147,0.322 -1.295,-0.271 -2.056,-0.867 -2.708,-1.562 0.835,-0.131 1.287,-0.666 1.287,-0.666 -1.061,-0.013 -1.824,-0.3 -2.485,-0.699 -0.565,-0.614 -1.233,-1.202 -2.254,-1.631 -0.294,-0.125 -0.606,-0.21 -0.922,-0.276 -0.086,-0.025 -0.178,-0.063 -0.258,-0.073 -0.906,-0.114 -1.845,0.051 -2.737,0.603 -0.322,0.2 -0.214,0.639 0.117,0.623 1.741,-0.085 2.866,0.582 3.47,1.633 2.169,3.772 5.344,3.875 5.344,3.875s-1.29,-0.688 -2.001,-2.149z" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml new file mode 100644 index 0000000000000000000000000000000000000000..8bb37a35bbdd5a4e174451cffdbd7845810edc5c --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M21,18c0,-2.001 3.246,-3.369 5,-6 2,-3 2,-10 2,-10H8s0,7 2,10c1.754,2.631 5,3.999 5,6s-3.246,3.369 -5,6c-2,3 -2,10 -2,10h20s0,-7 -2,-10c-1.754,-2.631 -5,-3.999 -5,-6z" + android:fillColor="#FFE8B6"/> + <path + android:pathData="M20.999,24c-0.999,0 -2.057,-1 -2.057,-2C19,20.287 19,19.154 19,18c0,-3.22 3.034,-4.561 4.9,-7H12.1c1.865,2.439 4.9,3.78 4.9,7 0,1.155 0,2.289 0.058,4 0,1 -1.058,2 -2.058,2 -2,0 -3.595,1.784 -4,3 -1,3 -1,7 -1,7h16s0,-4 -1,-7c-0.405,-1.216 -2.001,-3 -4.001,-3z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M30,34c0,1.104 -0.896,2 -2,2L8,36c-1.104,0 -2,-0.896 -2,-2s0.896,-2 2,-2h20c1.104,0 2,0.896 2,2zM30,2c0,1.104 -0.896,2 -2,2L8,4c-1.104,0 -2,-0.896 -2,-2s0.896,-2 2,-2h20c1.104,0 2,0.896 2,2z" + android:fillColor="#3B88C3"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cd1d033f7a15910f8d39bf8ab372bb54927026b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M32.614,3.414C28.31,-0.89 21.332,-0.89 17.027,3.414c-3.391,3.392 -4.098,8.439 -2.144,12.535l-3.916,3.915c-0.64,0.641 -0.841,1.543 -0.625,2.359l-1.973,1.972c-0.479,-0.48 -1.252,-0.48 -1.731,0l-1.731,1.732c-0.479,0.479 -0.479,1.253 0,1.732l-0.867,0.864c-0.479,-0.478 -1.253,-0.478 -1.731,0l-0.866,0.867c-0.479,0.479 -0.479,1.253 0,1.732 0.015,0.016 0.036,0.02 0.051,0.033 -0.794,1.189 -0.668,2.812 0.382,3.863 1.195,1.195 3.134,1.195 4.329,0L20.08,21.144c4.097,1.955 9.144,1.247 12.535,-2.146 4.302,-4.302 4.302,-11.28 -0.001,-15.584zM30.883,8.609c-0.957,0.956 -2.509,0.956 -3.464,0 -0.956,-0.956 -0.956,-2.507 0,-3.464 0.955,-0.956 2.507,-0.956 3.464,0 0.956,0.957 0.956,2.508 0,3.464z" + android:fillColor="#C1694F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml new file mode 100644 index 0000000000000000000000000000000000000000..18f31495002c4ed64c714db4340575063d2e659b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M29,11.06c0,6.439 -5,7.439 -5,13.44 0,3.098 -3.123,3.359 -5.5,3.359 -2.053,0 -6.586,-0.779 -6.586,-3.361C11.914,18.5 7,17.5 7,11.06 7,5.029 12.285,0.14 18.083,0.14 23.883,0.14 29,5.029 29,11.06z" + android:fillColor="#FFD983"/> + <path + android:pathData="M22.167,32.5c0,0.828 -2.234,2.5 -4.167,2.5 -1.933,0 -4.167,-1.672 -4.167,-2.5 0,-0.828 2.233,-0.5 4.167,-0.5 1.933,0 4.167,-0.328 4.167,0.5z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M22.707,10.293c-0.391,-0.391 -1.023,-0.391 -1.414,0L18,13.586l-3.293,-3.293c-0.391,-0.391 -1.023,-0.391 -1.414,0s-0.391,1.023 0,1.414L17,15.414V26c0,0.553 0.448,1 1,1s1,-0.447 1,-1V15.414l3.707,-3.707c0.391,-0.391 0.391,-1.023 0,-1.414z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M24,31c0,1.104 -0.896,2 -2,2h-8c-1.104,0 -2,-0.896 -2,-2v-6h12v6z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M11.999,32c-0.48,0 -0.904,-0.347 -0.985,-0.836 -0.091,-0.544 0.277,-1.06 0.822,-1.15l12,-2c0.544,-0.098 1.06,0.277 1.15,0.822 0.091,0.544 -0.277,1.06 -0.822,1.15l-12,2c-0.055,0.01 -0.111,0.014 -0.165,0.014zM11.999,28c-0.48,0 -0.904,-0.347 -0.985,-0.836 -0.091,-0.544 0.277,-1.06 0.822,-1.15l12,-2c0.544,-0.097 1.06,0.277 1.15,0.822 0.091,0.544 -0.277,1.06 -0.822,1.15l-12,2c-0.055,0.01 -0.111,0.014 -0.165,0.014z" + android:fillColor="#CCD6DD"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml new file mode 100644 index 0000000000000000000000000000000000000000..b97a508fc28dc82708f1477e25d3bf4d381794e4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml @@ -0,0 +1,54 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M32.325,10.958s2.315,0.024 3.511,1.177c-0.336,-4.971 -2.104,-8.249 -5.944,-10.13 -3.141,-1.119 -6.066,1.453 -6.066,1.453s0.862,-1.99 2.19,-2.746C23.789,0.236 21.146,0 18,0c-3.136,0 -5.785,0.227 -8.006,0.701 1.341,0.745 2.215,2.758 2.215,2.758S9.194,0.803 6,2.053C2.221,3.949 0.481,7.223 0.158,12.174c1.183,-1.19 3.55,-1.215 3.55,-1.215S-0.105,13.267 0.282,16.614c0.387,2.947 1.394,5.967 2.879,8.722C3.039,22.15 5.917,20 5.917,20s-2.492,5.96 -0.581,8.738c1.935,2.542 4.313,4.641 6.976,5.916 -0.955,-1.645 -0.136,-3.044 -0.103,-2.945 0.042,0.125 0.459,3.112 2.137,3.743 1.178,0.356 2.4,0.548 3.654,0.548 1.292,0 2.55,-0.207 3.761,-0.583 1.614,-0.691 2.024,-3.585 2.064,-3.708 0.032,-0.098 0.843,1.287 -0.09,2.921 2.706,-1.309 5.118,-3.463 7.064,-6.073 1.699,-2.846 -0.683,-8.557 -0.683,-8.557s2.85,2.13 2.757,5.288c1.556,-2.906 2.585,-6.104 2.911,-9.2 -0.035,-3.061 -3.459,-5.13 -3.459,-5.13z" + android:fillColor="#662113"/> + <path + android:pathData="M13.859,9.495c0.596,2.392 0.16,4.422 -2.231,5.017 -2.392,0.596 -6.363,0.087 -6.958,-2.304 -0.596,-2.392 0.469,-5.39 1.81,-5.724 1.341,-0.334 6.784,0.62 7.379,3.011zM22.963,27.927c0,2.74 -2.222,4.963 -4.963,4.963s-4.963,-2.223 -4.963,-4.963c0,-2.741 2.223,-4.964 4.963,-4.964 2.741,0 4.963,2.222 4.963,4.964z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M21.309,27.927c0,1.827 -1.481,3.309 -3.309,3.309s-3.309,-1.481 -3.309,-3.309c0,-1.827 1.481,-3.31 3.309,-3.31s3.309,1.483 3.309,3.31z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M11.052,8.997c0.871,1.393 0.447,3.229 -0.946,4.1 -1.394,0.871 -2.608,0.797 -3.479,-0.596 -0.871,-1.394 -0.186,-4.131 0.324,-4.45 0.51,-0.319 3.23,-0.448 4.101,0.946z" + android:fillColor="#E6AAAA"/> + <path + android:pathData="M22.141,9.495c-0.596,2.392 -0.159,4.422 2.232,5.017 2.392,0.596 6.363,0.087 6.959,-2.304 0.596,-2.392 -0.47,-5.39 -1.811,-5.724 -1.342,-0.334 -6.786,0.62 -7.38,3.011z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M24.948,8.997c-0.871,1.393 -0.447,3.229 0.945,4.1 1.394,0.871 2.608,0.797 3.479,-0.596 0.871,-1.394 0.185,-4.131 -0.324,-4.45 -0.51,-0.319 -3.229,-0.448 -4.1,0.946z" + android:fillColor="#E6AAAA"/> + <path + android:pathData="M18,7.125h-0.002C5.167,7.126 7.125,12.083 8.5,18.667 9.875,25.25 10.384,27 10.384,27h15.228s0.51,-1.75 1.885,-8.333C28.872,12.083 30.829,7.126 18,7.125z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M12,16s0,-1.5 1.5,-1.5S15,16 15,16v1.5s0,1.5 -1.5,1.5 -1.5,-1.5 -1.5,-1.5L12,16zM21,16s0,-1.5 1.5,-1.5S24,16 24,16v1.5s0,1.5 -1.5,1.5 -1.5,-1.5 -1.5,-1.5L21,16z" + android:fillColor="#272B2B"/> + <path + android:pathData="M20.168,21.521c-1.598,0 -1.385,0.848 -2.168,2.113 -0.783,-1.266 -0.571,-2.113 -2.168,-2.113 -6.865,0 -6.837,0.375 -6.865,2.828 -0.058,4.986 2.802,6.132 5.257,6.06 1.597,-0.048 2.994,-0.88 3.777,-2.131 0.783,1.251 2.179,2.083 3.776,2.131 2.455,0.072 5.315,-1.073 5.257,-6.06 -0.029,-2.453 -0.001,-2.828 -6.866,-2.828z" + android:fillColor="#FFE8B6"/> + <path + android:pathData="M14.582,21.411c-1.14,0.233 2.279,4.431 3.418,4.431s4.559,-4.198 3.419,-4.431c-1.14,-0.232 -5.698,-0.232 -6.837,0z" + android:fillColor="#272B2B"/> + <path + android:pathData="M11.5,24.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M10.5,26.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M12.5,27.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M24.5,24.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M25.5,26.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> + <path + android:pathData="M23.5,27.5m-0.5,0a0.5,0.5 0,1 1,1 0a0.5,0.5 0,1 1,-1 0" + android:fillColor="#D99E82"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml new file mode 100644 index 0000000000000000000000000000000000000000..de3979434fd8bb35ee442326a9fa8a1d701c094e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,3C12.477,3 8,7.477 8,13v10h4V13c0,-3.313 2.686,-6 6,-6s6,2.687 6,6v10h4V13c0,-5.523 -4.477,-10 -10,-10z" + android:fillColor="#AAB8C2"/> + <path + android:pathData="M31,32c0,2.209 -1.791,4 -4,4H9c-2.209,0 -4,-1.791 -4,-4V20c0,-2.209 1.791,-4 4,-4h18c2.209,0 4,1.791 4,4v12z" + android:fillColor="#FFAC33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml new file mode 100644 index 0000000000000000000000000000000000000000..3f5abe6ae31c7a3e71b4fd464070910642ffd820 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M30.312,0.776C32,19 20,32 0.776,30.312c8.199,7.717 21.091,7.588 29.107,-0.429C37.9,21.867 38.03,8.975 30.312,0.776z" + android:fillColor="#FFD983"/> + <path + android:pathData="M30.705,15.915c-0.453,0.454 -0.453,1.189 0,1.644 0.454,0.453 1.189,0.453 1.643,0 0.454,-0.455 0.455,-1.19 0,-1.644 -0.453,-0.454 -1.189,-0.454 -1.643,0zM14.683,30.295c-0.682,0.681 -0.682,1.783 0,2.465 0.68,0.682 1.784,0.682 2.464,0 0.681,-0.682 0.681,-1.784 0,-2.465 -0.68,-0.682 -1.784,-0.682 -2.464,0zM28.651,28.148c-1.135,1.135 -2.974,1.135 -4.108,0 -1.135,-1.135 -1.135,-2.975 0,-4.107 1.135,-1.136 2.974,-1.136 4.108,0 1.135,1.133 1.135,2.973 0,4.107z" + android:fillColor="#FFCC4D"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml new file mode 100644 index 0000000000000000000000000000000000000000..72f70368561d76e7f36583ccbd5658a31b836c0e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M27,33c0,2.209 -1.791,3 -4,3H13c-2.209,0 -4,-0.791 -4,-3s3,-7 3,-13 12,-6 12,0 3,10.791 3,13z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M34.666,11.189l-0.001,-0.002c-0.96,-2.357 -2.404,-4.453 -4.208,-6.182h-0.003C27.222,1.904 22.839,0 18,0 13.638,0 9.639,1.541 6.524,4.115c-2.19,1.809 -3.941,4.13 -5.076,6.785C0.518,13.075 0,15.473 0,18c0,2.209 1.791,4 4,4h28c2.209,0 4,-1.791 4,-4 0,-2.417 -0.48,-4.713 -1.334,-6.811z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M7.708,16.583c3.475,0 6.292,-2.817 6.292,-6.292S11.184,4 7.708,4c-0.405,0 -0.8,0.042 -1.184,0.115 -2.19,1.809 -3.941,4.13 -5.076,6.785 0.306,3.189 2.991,5.683 6.26,5.683z" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M7.708,4.25c3.331,0 6.041,2.71 6.041,6.042s-2.71,6.042 -6.041,6.042c-3.107,0 -5.678,-2.314 -6.006,-5.394 1.097,-2.541 2.8,-4.817 4.931,-6.59 0.364,-0.067 0.726,-0.1 1.075,-0.1m0,-0.25c-0.405,0 -0.8,0.042 -1.184,0.115 -2.19,1.809 -3.941,4.13 -5.076,6.785 0.306,3.189 2.992,5.683 6.261,5.683 3.475,0 6.291,-2.817 6.291,-6.292S11.184,4 7.708,4zM26,9.5c0,2.485 2.015,4.5 4.5,4.5 1.887,0 3.497,-1.164 4.166,-2.811l-0.001,-0.002c-0.96,-2.357 -2.404,-4.453 -4.208,-6.182C27.992,5.028 26,7.029 26,9.5z" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M21.5,16m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M20,5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" + android:fillColor="#F4ABBA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml new file mode 100644 index 0000000000000000000000000000000000000000..054760f3b838078696ba38af8f1e9c68ec522b60 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M10,12c3,5 0,10.692 -3,9.692s-4,2 -1,3 9.465,-0.465 13,-4c1,-1 2,-1 2,-1L10,12z" + android:fillColor="#553788"/> + <path + android:pathData="M26,12c-3,5 0,10.692 3,9.692s4,2 1,3 -9.465,-0.465 -13,-4c-1,-1 -2,-1 -2,-1L26,12z" + android:fillColor="#553788"/> + <path + android:pathData="M30.188,16c-3,5 0,10.692 3,9.692s4,2 1,3 -9.465,-0.465 -13,-4c-1,-1 -2,-1 -2,-1l11,-7.692zM5.812,16c3,5 0,10.692 -3,9.692s-4,2 -1,3 9.465,-0.465 13,-4c1,-1 2,-1 2,-1L5.812,16z" + android:fillColor="#744EAA"/> + <path + android:pathData="M33.188,31.375c-2.729,0.91 -6.425,-5.626 -4.812,-10.578C30.022,17.554 31,13.94 31,11c0,-7.18 -5.82,-11 -13,-11S5,3.82 5,11c0,2.94 0.978,6.554 2.624,9.797 1.613,4.952 -2.083,11.488 -4.812,10.578 -3,-1 -4,3 -1,4s8.31,-0.627 12,-4c2.189,-2 4.189,-2 4.189,-2s2,0 4.188,2c3.69,3.373 9,5 12,4s1.999,-5 -1.001,-4z" + android:fillColor="#9266CC"/> + <path + android:pathData="M14,21m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#292F33"/> + <path + android:pathData="M22,21m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml new file mode 100644 index 0000000000000000000000000000000000000000..ab1e718c4421d1d7ee94e92f8d7e9b5b0b0cc70b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml @@ -0,0 +1,42 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M7,6m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:fillColor="#272B2B"/> + <path + android:pathData="M29,6m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:fillColor="#272B2B"/> + <path + android:pathData="M7,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#66757F"/> + <path + android:pathData="M29,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#66757F"/> + <path + android:pathData="M35,22c0,7 -6.375,12 -17,12S1,29 1,22C1,22 2.308,0 18,0s17,22 17,22z" + android:fillColor="#EEE"/> + <path + android:pathData="M18,30m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M18,30m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#DD2E44"/> + <path + android:pathData="M20.709,12.654C25.163,9.878 32,17 26.952,22.67 23.463,26.591 20,25 20,25s-2.636,-10.26 0.709,-12.346zM15.267,12.665C10.813,9.888 3.976,17.01 9.023,22.681c3.49,3.92 6.953,2.329 6.953,2.329s2.636,-10.26 -0.709,-12.345z" + android:fillColor="#272B2B"/> + <path + android:pathData="M11,17s0,-2 2,-2 2,2 2,2v2s0,2 -2,2 -2,-2 -2,-2v-2z" + android:fillColor="#66757F"/> + <path + android:pathData="M18,20S7,23.687 7,27s2.687,6 6,6c2.088,0 3.925,-1.067 5,-2.685C19.074,31.933 20.912,33 23,33c3.313,0 6,-2.687 6,-6s-11,-7 -11,-7z" + android:fillColor="#FFF"/> + <path + android:pathData="M21,17s0,-2 2,-2 2,2 2,2v2s0,2 -2,2 -2,-2 -2,-2v-2z" + android:fillColor="#66757F"/> + <path + android:pathData="M13.125,25c-1.624,1 3.25,4 4.875,4s6.499,-3 4.874,-4 -8.124,-1 -9.749,0z" + android:fillColor="#272B2B"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml new file mode 100644 index 0000000000000000000000000000000000000000..e8f89859d606cc23fc1fb5a610f34b87ca0c6b61 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35.354,25.254c0.217,-2.391 -0.513,-4.558 -2.057,-6.102L17.033,2.89c-0.391,-0.391 -1.024,-0.391 -1.414,0 -0.391,0.391 -0.391,1.024 0,1.414l16.264,16.263c1.116,1.117 1.642,2.717 1.479,4.506 -0.159,1.748 -0.957,3.456 -2.188,4.686 -1.23,1.23 -2.938,2.027 -4.685,2.187 -1.781,0.161 -3.39,-0.362 -4.506,-1.479L3.598,12.082c-0.98,-0.98 -1.059,-2.204 -0.953,-3.058 0.15,-1.196 0.755,-2.401 1.66,-3.307 1.7,-1.7 4.616,-2.453 6.364,-0.707l14.85,14.849c1.119,1.12 0.026,2.803 -0.708,3.536 -0.733,0.735 -2.417,1.826 -3.535,0.707L9.962,12.789c-0.391,-0.391 -1.024,-0.39 -1.414,0 -0.391,0.391 -0.391,1.023 0,1.414l11.313,11.314c1.859,1.858 4.608,1.05 6.363,-0.707 1.758,-1.757 2.565,-4.507 0.708,-6.364L12.083,3.597c-2.62,-2.62 -6.812,-1.673 -9.192,0.706C1.677,5.517 0.864,7.147 0.661,8.775c-0.229,1.833 0.312,3.509 1.523,4.721l18.384,18.385c1.365,1.365 3.218,2.094 5.281,2.094 0.27,0 0.544,-0.013 0.82,-0.037 2.206,-0.201 4.362,-1.209 5.918,-2.765 1.558,-1.556 2.565,-3.713 2.767,-5.919z" + android:fillColor="#99AAB5"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b9f51fca50ab067085b340956bb6803673cb551 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M35.222,33.598c-0.647,-2.101 -1.705,-6.059 -2.325,-7.566 -0.501,-1.216 -0.969,-2.438 -1.544,-3.014 -0.575,-0.575 -1.553,-0.53 -2.143,0.058 0,0 -2.469,1.675 -3.354,2.783 -1.108,0.882 -2.785,3.357 -2.785,3.357 -0.59,0.59 -0.635,1.567 -0.06,2.143 0.576,0.575 1.798,1.043 3.015,1.544 1.506,0.62 5.465,1.676 7.566,2.325 0.359,0.11 1.74,-1.271 1.63,-1.63z" + android:fillColor="#D99E82"/> + <path + android:pathData="M13.643,5.308c1.151,1.151 1.151,3.016 0,4.167l-4.167,4.168c-1.151,1.15 -3.018,1.15 -4.167,0L1.141,9.475c-1.15,-1.151 -1.15,-3.016 0,-4.167l4.167,-4.167c1.15,-1.151 3.016,-1.151 4.167,0l4.168,4.167z" + android:fillColor="#EA596E"/> + <path + android:pathData="M31.353,23.018l-4.17,4.17 -4.163,4.165L7.392,15.726l8.335,-8.334 15.626,15.626z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M32.078,34.763s2.709,1.489 3.441,0.757c0.732,-0.732 -0.765,-3.435 -0.765,-3.435s-2.566,0.048 -2.676,2.678z" + android:fillColor="#292F33"/> + <path + android:pathData="M2.183,10.517l8.335,-8.335 5.208,5.209 -8.334,8.335z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M3.225,11.558l8.334,-8.334 1.042,1.042L4.267,12.6zM5.308,13.644l8.335,-8.335 1.042,1.042 -8.335,8.334z" + android:fillColor="#99AAB5"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml new file mode 100644 index 0000000000000000000000000000000000000000..fb2e05760f353384121fc547c6acc2982f418a18 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M28.068,31.355c-2.229,0 -8.468,0.785 -10.068,1.832 -1.601,-1.047 -7.84,-1.832 -10.069,-1.832 -2.564,0 -1.161,1.039 -1.161,2.322C6.77,34.96 5.367,36 7.931,36c2.229,0 8.468,-0.785 10.069,-1.832C19.601,35.215 25.84,36 28.068,36c2.565,0 1.161,-1.04 1.161,-2.322 0,-1.283 1.405,-2.323 -1.161,-2.323z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M31.73,15.866c-1.25,-2.499 -3.152,-4.995 -4.942,-6.723C24.337,3.711 20.759,0 18,0s-6.337,3.71 -8.788,9.143c-1.791,1.729 -3.693,4.224 -4.943,6.724 -2.438,4.876 -3.116,11.426 -2.078,11.944 0.873,0.437 2.324,-1.552 4.208,-5.082C6.667,33.604 13.446,33.678 18,33.678c4.553,0 11.333,-0.073 11.601,-10.947 1.884,3.528 3.335,5.517 4.207,5.08 1.038,-0.519 0.361,-7.069 -2.078,-11.945z" + android:fillColor="#292F33"/> + <path + android:pathData="M21.675,6.943c-0.85,0.607 -2.172,1.186 -3.675,1.186s-2.825,-0.578 -3.675,-1.185c-3.302,2.137 -5.615,7.06 -5.615,12.798 0,7.695 4.159,13.936 9.29,13.936 5.132,0 9.291,-6.24 9.291,-13.936 0,-5.738 -2.313,-10.662 -5.616,-12.799z" + android:fillColor="#F5F8FA"/> + <path + android:pathData="M28.452,6h-5.808C18.797,6 18,5.22 18,4.257c0,-0.962 -0.364,-1.742 3.483,-1.742C27.291,2.516 29.613,6 28.452,6z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M16.839,3.483c0,0.642 -0.52,1.162 -1.161,1.162 -0.642,0 -1.161,-0.521 -1.161,-1.162 0,-0.641 0.52,-1.161 1.161,-1.161s1.161,0.52 1.161,1.161z" + android:fillColor="#F5F8FA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml new file mode 100644 index 0000000000000000000000000000000000000000..7beda09c4ec5f8255d2459f8a1e1923aefef59ef --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M36,11.5C36,8.462 33,4 18,4S0,8.462 0,11.5c0,0.045 0.019,0.076 0.022,0.119 -0.012,0.196 -0.022,0.402 -0.022,0.631C0,14.873 2.239,16 5,16s5,-1.127 5,-3.75c0,-0.218 -0.021,-0.412 -0.051,-0.597C12.374,11.302 15.102,11 18,11s5.626,0.302 8.051,0.653c-0.03,0.185 -0.051,0.379 -0.051,0.597 0,2.623 2.238,3.75 5,3.75s5,-1.127 5,-3.75c0,-0.225 -0.009,-0.429 -0.024,-0.621 0.004,-0.046 0.024,-0.08 0.024,-0.129z" + android:fillColor="#BE1931"/> + <path + android:pathData="M34.934,23c-0.482,-1.031 -2.31,-4.19 -3.968,-7.007C29.408,13.346 27,11 25,11V9c0,-1.104 -0.896,-2 -2,-2s-2,0.896 -2,2v2h-6V9c0,-1.104 -0.896,-2 -2,-2s-2,0.896 -2,2v2c-2,0 -4.41,2.351 -5.97,5 -1.657,2.815 -3.483,5.97 -3.964,7C0.488,24.239 0,25 0,27s1.791,5 4,5h28c2.209,0 4,-3 4,-5s-0.448,-2.676 -1.066,-4z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M20.046,14.818c0,0.452 -0.916,0.818 -2.046,0.818s-2.045,-0.366 -2.045,-0.818c0,-0.452 0.915,-0.818 2.045,-0.818s2.046,0.366 2.046,0.818zM15.136,14.818c0,0.452 -0.915,0.818 -2.045,0.818s-2.045,-0.366 -2.045,-0.818c0,-0.452 0.916,-0.818 2.045,-0.818s2.045,0.366 2.045,0.818zM24.954,14.818c0,0.452 -0.915,0.818 -2.045,0.818s-2.046,-0.366 -2.046,-0.818c0,-0.452 0.916,-0.818 2.046,-0.818s2.045,0.366 2.045,0.818zM20.454,17.682c0,0.679 -1.099,1.228 -2.454,1.228s-2.455,-0.549 -2.455,-1.228c0,-0.677 1.099,-1.227 2.455,-1.227s2.454,0.549 2.454,1.227zM26.182,17.682c0,0.679 -1.1,1.228 -2.454,1.228 -1.355,0 -2.455,-0.549 -2.455,-1.228 0,-0.677 1.1,-1.227 2.455,-1.227 1.354,0 2.454,0.549 2.454,1.227zM14.727,17.682c0,0.679 -1.099,1.228 -2.454,1.228 -1.355,0 -2.455,-0.549 -2.455,-1.228 0,-0.677 1.099,-1.227 2.455,-1.227 1.355,0 2.454,0.549 2.454,1.227zM21.272,21.363C21.272,22.269 19.807,23 18,23c-1.807,0 -3.273,-0.731 -3.273,-1.637 0,-0.903 1.466,-1.636 3.273,-1.636 1.807,0.001 3.272,0.733 3.272,1.636zM28.637,21.363c0,0.905 -1.467,1.637 -3.273,1.637 -1.807,0 -3.273,-0.731 -3.273,-1.637 0,-0.903 1.466,-1.636 3.273,-1.636 1.806,0.001 3.273,0.733 3.273,1.636zM13.909,21.363c0,0.905 -1.466,1.637 -3.273,1.637 -1.807,0 -3.272,-0.731 -3.272,-1.637 0,-0.903 1.465,-1.636 3.272,-1.636 1.807,0.001 3.273,0.733 3.273,1.636z" + android:fillColor="#FFF"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml new file mode 100644 index 0000000000000000000000000000000000000000..c31bd06c52c728a9720fe8b4161270b42c8839e9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M34.193,13.329c0.387,-0.371 0.733,-0.795 1.019,-1.28 1.686,-2.854 0.27,-10.292 -0.592,-10.8 -0.695,-0.411 -5.529,1.05 -8.246,3.132C23.876,2.884 21.031,2 18,2c-3.021,0 -5.856,0.879 -8.349,2.367C6.93,2.293 2.119,0.839 1.424,1.249c-0.861,0.508 -2.276,7.947 -0.592,10.8 0.278,0.471 0.615,0.884 0.989,1.249C0.666,15.85 0,18.64 0,21.479 0,31.468 8.011,34 18,34s18,-2.532 18,-12.521c0,-2.828 -0.66,-5.606 -1.807,-8.15z" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M7.398,5.965c-2.166,-1.267 -4.402,-2.08 -4.8,-1.845 -0.57,0.337 -1.083,4.998 -0.352,8.265 1.273,-2.483 3.04,-4.682 5.152,-6.42zM33.753,12.384c0.733,-3.267 0.219,-7.928 -0.351,-8.265 -0.398,-0.235 -2.635,0.578 -4.801,1.845 2.114,1.739 3.88,3.938 5.152,6.42zM28,23.125c0,4.487 -3.097,9.375 -10,9.375 -6.904,0 -10,-4.888 -10,-9.375S11.096,17.5 18,17.5c6.903,0 10,1.138 10,5.625z" + android:fillColor="#EA596E"/> + <path + android:pathData="M15,24.6c0,1.857 -0.34,2.4 -1.5,2.4s-1.5,-0.543 -1.5,-2.4c0,-1.856 0.34,-2.399 1.5,-2.399s1.5,0.542 1.5,2.399zM24,24.6c0,1.857 -0.34,2.4 -1.5,2.4s-1.5,-0.543 -1.5,-2.4c0,-1.856 0.34,-2.399 1.5,-2.399s1.5,0.542 1.5,2.399z" + android:fillColor="#662113"/> + <path + android:pathData="M7,17m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#292F33"/> + <path + android:pathData="M29,17m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml new file mode 100644 index 0000000000000000000000000000000000000000..f10e4606a95dec5beee7c0036aca55da316c63dc --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M23.651,23.297L12.702,12.348l9.386,-7.821 9.385,9.385z" + android:fillColor="#BE1931"/> + <path + android:pathData="M34.6,13.912c-1.727,1.729 -4.528,1.729 -6.255,0l-6.257,-6.256c-1.729,-1.727 -1.729,-4.53 0,-6.258 1.726,-1.727 4.528,-1.727 6.257,0L34.6,7.656c1.728,1.727 1.728,4.529 0,6.256z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M14,17.823S-0.593,35.029 0.188,35.813C0.97,36.596 18.177,22 18.177,22L14,17.823z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M25.215,27.991c-1.726,1.729 -4.528,1.729 -6.258,0L8.009,17.041c-1.727,-1.728 -1.727,-4.528 0,-6.256 1.728,-1.729 4.53,-1.729 6.258,0l10.948,10.949c1.728,1.729 1.728,4.528 0,6.257z" + android:fillColor="#DD2E44"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml new file mode 100644 index 0000000000000000000000000000000000000000..a514aeb3d6e25988eb14b83ad65ea9834dbda5a7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,4c7.257,0 13,4 14.699,2 0.197,-0.323 0.301,-0.657 0.301,-1 0,-2 -6.716,-5 -15,-5C9.716,0 3,3 3,5c0,0.343 0.104,0.677 0.301,1C5,8 10.743,4 18,4z" + android:fillColor="#F4900C"/> + <path + android:pathData="M18,3C11.787,3 7.384,4.81 5.727,5.618c-0.477,0.233 -0.539,0.84 -0.415,1.278S16,34 16,34s0.896,2 2,2 2,-2 2,-2L30.704,6.779s0.213,-0.842 -0.569,-1.229C28.392,4.689 24.047,3 18,3z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M18,31c0,-2.208 -1.791,-4 -4,-4 -0.254,0 -0.5,0.029 -0.741,0.075L16,34s0.071,0.14 0.19,0.342C17.279,33.627 18,32.399 18,31zM17,20c0,-2.209 -1.792,-4 -4,-4 -1.426,0 -2.67,0.752 -3.378,1.876l2.362,5.978c0.327,0.086 0.663,0.146 1.016,0.146 2.208,0 4,-1.792 4,-4z" + android:fillColor="#BE1931"/> + <path + android:pathData="M16,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#BE1931"/> + <path + android:pathData="M25,9c-2.208,0 -4,1.791 -4,4s1.792,4 4,4c0.682,0 1.315,-0.187 1.877,-0.488l1.89,-4.806C28.227,10.135 26.752,9 25,9zM19,25c0,1.868 1.288,3.425 3.019,3.864l2.893,-7.357C24.342,21.194 23.697,21 23,21c-2.208,0 -4,1.792 -4,4zM10,12c0,-2.209 -1.791,-4 -4,-4 -0.087,0 -0.169,0.02 -0.255,0.026 0.55,1.412 1.575,4.016 2.775,7.057C9.416,14.349 10,13.248 10,12z" + android:fillColor="#BE1931"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml new file mode 100644 index 0000000000000000000000000000000000000000..c8ff75c999b0429b164c9fa0c4b45155c5ac6412 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml @@ -0,0 +1,27 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M33.799,0.005c-0.467,-0.178 -7.998,3.971 -9.969,9.131 -1.166,3.052 -1.686,6.058 -1.652,8.112C20.709,16.459 19.257,16 18,16s-2.709,0.458 -4.178,1.249c0.033,-2.055 -0.486,-5.061 -1.652,-8.112C10.2,3.977 2.668,-0.173 2.201,0.005c-0.455,0.174 4.268,16.044 7.025,20.838C6.805,23.405 5,26.661 5,29.828c0,3.234 1.635,5.14 4,5.94 2.531,0.857 5,-0.94 9,-0.94s6.469,1.798 9,0.94c2.365,-0.801 4,-2.706 4,-5.94 0,-3.166 -1.805,-6.423 -4.225,-8.984C29.53,16.049 34.255,0.179 33.799,0.005z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M12.692,17.922c-0.178,-1.54 -0.68,-3.55 -1.457,-5.584 -1.534,-4.016 -5.686,-7.245 -6.049,-7.107 -0.319,0.122 2.627,10.14 4.783,14.863 0.866,-0.824 1.786,-1.563 2.723,-2.172zM26.03,20.094c2.156,-4.723 5.102,-14.741 4.784,-14.862 -0.363,-0.139 -4.516,3.091 -6.05,7.107 -0.777,2.034 -1.279,4.043 -1.457,5.583 0.937,0.609 1.857,1.348 2.723,2.172z" + android:fillColor="#F4ABBA"/> + <path + android:pathData="M25,30c0,2.762 -3.06,5 -6.834,5 -3.773,0 -6.833,-2.238 -6.833,-5s3.06,-5 6.833,-5C21.94,25 25,27.238 25,30z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M21,30.578c0,2.762 -0.238,3 -3,3 -2.761,0 -3,-0.238 -3,-3 0,-1 6,-1 6,0z" + android:fillColor="#FFF"/> + <path + android:pathData="M12.5,24.328m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:fillColor="#292F33"/> + <path + android:pathData="M23.5,24.328m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:fillColor="#292F33"/> + <path + android:pathData="M21,25.828c0,1.657 -2,3 -3,3s-3,-1.343 -3,-3 6,-1.657 6,0z" + android:fillColor="#F4ABBA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml new file mode 100644 index 0000000000000000000000000000000000000000..a53cfe99c01876235ee1654c78a73a4f5f437cf0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml @@ -0,0 +1,48 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M31,14.5a2.5,3.5 0,1 0,5 0a2.5,3.5 0,1 0,-5 0z" + android:fillColor="#F4900C"/> + <path + android:pathData="M0,14.5a2.5,3.5 0,1 0,5 0a2.5,3.5 0,1 0,-5 0z" + android:fillColor="#F4900C"/> + <path + android:pathData="M34,19c0,0.553 -0.447,1 -1,1h-3c-0.553,0 -1,-0.447 -1,-1v-9c0,-0.552 0.447,-1 1,-1h3c0.553,0 1,0.448 1,1v9zM7,19c0,0.553 -0.448,1 -1,1H3c-0.552,0 -1,-0.447 -1,-1v-9c0,-0.552 0.448,-1 1,-1h3c0.552,0 1,0.448 1,1v9z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M28,5c0,2.761 -4.478,4 -10,4C12.477,9 8,7.761 8,5s4.477,-5 10,-5c5.522,0 10,2.239 10,5z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M25,4.083C25,5.694 21.865,7 18,7c-3.866,0 -7,-1.306 -7,-2.917 0,-1.611 3.134,-2.917 7,-2.917 3.865,0 7,1.306 7,2.917z" + android:fillColor="#F4900C"/> + <path + android:pathData="M30,5.5C30,6.881 28.881,7 27.5,7h-19C7.119,7 6,6.881 6,5.5S7.119,3 8.5,3h19C28.881,3 30,4.119 30,5.5z" + android:fillColor="#269"/> + <path + android:pathData="M30,6H6c-1.104,0 -2,0.896 -2,2v26h28V8c0,-1.104 -0.896,-2 -2,-2z" + android:fillColor="#55ACEE"/> + <path + android:pathData="M35,33v-1c0,-1.104 -0.896,-2 -2,-2H22.071l-3.364,3.364c-0.391,0.391 -1.023,0.391 -1.414,0L13.929,30H3c-1.104,0 -2,0.896 -2,2v1c0,1.104 -0.104,2 1,2h32c1.104,0 1,-0.896 1,-2z" + android:fillColor="#3B88C3"/> + <path + android:pathData="M24.5,14.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" + android:fillColor="#FFF"/> + <path + android:pathData="M24.5,14.5m-2.721,0a2.721,2.721 0,1 1,5.442 0a2.721,2.721 0,1 1,-5.442 0" + android:fillColor="#DD2E44"/> + <path + android:pathData="M11.5,14.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" + android:fillColor="#FFF"/> + <path + android:pathData="M29,25.5c0,1.381 -1.119,2.5 -2.5,2.5h-17C8.119,28 7,26.881 7,25.5S8.119,23 9.5,23h17c1.381,0 2.5,1.119 2.5,2.5z" + android:fillColor="#F5F8FA"/> + <path + android:pathData="M17,23h2v5h-2zM12,23h2v5h-2zM22,23h2v5h-2zM7,25.5c0,1.21 0.859,2.218 2,2.45v-4.9c-1.141,0.232 -2,1.24 -2,2.45zM27,23.05v4.899c1.141,-0.232 2,-1.24 2,-2.45s-0.859,-2.217 -2,-2.449z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M11.5,14.5m-2.721,0a2.721,2.721 0,1 1,5.442 0a2.721,2.721 0,1 1,-5.442 0" + android:fillColor="#DD2E44"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml new file mode 100644 index 0000000000000000000000000000000000000000..4097ed9030d6c9adb05d3ba3fcd6361f6e0b2d22 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M1,17l8,-7 16,1 1,16 -7,8s0.001,-5.999 -6,-12 -12,-6 -12,-6z" + android:fillColor="#A0041E"/> + <path + android:pathData="M0.973,35s-0.036,-7.979 2.985,-11S15,21.187 15,21.187 14.999,29 11.999,32c-3,3 -11.026,3 -11.026,3z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M8.999,27m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M35.999,0s-10,0 -22,10c-6,5 -6,14 -4,16s11,2 16,-4c10,-12 10,-22 10,-22z" + android:fillColor="#55ACEE"/> + <path + android:fillColor="#FF000000" + android:pathData="M26.999,5c-1.623,0 -3.013,0.971 -3.641,2.36 0.502,-0.227 1.055,-0.36 1.641,-0.36 2.209,0 4,1.791 4,4 0,0.586 -0.133,1.139 -0.359,1.64 1.389,-0.627 2.359,-2.017 2.359,-3.64 0,-2.209 -1.791,-4 -4,-4z"/> + <path + android:pathData="M8,28s0,-4 1,-5 13.001,-10.999 14,-10 -9.001,13 -10.001,14S8,28 8,28z" + android:fillColor="#A0041E"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml new file mode 100644 index 0000000000000000000000000000000000000000..cb7ad563f0f17956172be9086fca28905cd54b20 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M24.88,33.097c-0.098,-0.18 -0.25,-0.302 -0.418,-0.391C22.865,31 24,28.999 24,28.999c0,-0.553 1,-2 0,-2l-1,1c-1,1 -1,4 -1,4h-2c-0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1h1.107l-0.222,0.12c-0.486,0.263 -0.667,0.869 -0.404,1.355s0.869,0.667 1.356,0.404l2.639,-1.427c0.486,-0.262 0.667,-0.868 0.404,-1.354zM17.88,33.097c-0.097,-0.18 -0.25,-0.302 -0.417,-0.391C15.866,31 17,28.999 17,28.999c0,-0.553 1,-2 0,-2l-1,1c-1,1 -1,4 -1,4h-2c-0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1h1.108l-0.222,0.12c-0.486,0.263 -0.667,0.869 -0.404,1.355s0.869,0.667 1.356,0.404l2.639,-1.427c0.485,-0.262 0.666,-0.868 0.403,-1.354zM7.516,10c0,1.104 -1.119,2 -2.5,2s-3.5,-1 -3.5,-2 2.119,-2 3.5,-2c1.38,0 2.5,0.896 2.5,2z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M13.516,2c-2,-1 -3,1 -3,1s0,-3 -3,-3 -3,3 -3,3 -3,-0.938 -3,2c0,1.482 1.101,2.411 2.484,2.387V12c0,1 0.263,3 -0.737,4s-2.484,4 0.516,4 3,-4 3,-7c1,1 4,1 4,-4 0,-0.867 -0.213,-1.512 -0.55,-2h1.287c4,0 4,-4 2,-5z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M32.516,9c4,10 0,22 -13,22 -7.732,0 -13,-6 -14,-11 -1.177,-5.883 -1,-8 -1,-12 0,-2.738 2.118,-4.824 5,-4 7,2 5,10 12,10 10,0 8.23,-11.923 11,-5z" + android:fillColor="#E1E8ED"/> + <path + android:pathData="M7.516,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#292F33"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml new file mode 100644 index 0000000000000000000000000000000000000000..4f7bc1a24f4229fbe80471e4dec707710d6b86ac --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml @@ -0,0 +1,42 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M5,21c0,2.209 -1.119,4 -2.5,4S0,23.209 0,21s1.119,-4 2.5,-4S5,18.791 5,21z" + android:fillColor="#FFDC5D"/> + <path + android:pathData="M3,18.562C3,10.037 8.373,3.125 15,3.125s12,6.912 12,15.438C27,27.088 21.627,34 15,34S3,27.088 3,18.562z" + android:fillColor="#FFDC5D"/> + <path + android:pathData="M20,0c-0.249,0 -0.478,0.007 -0.713,0.012C19.19,0.01 19.097,0 19,0 9,0 2,4.582 2,9s6.373,4 13,4c4.442,0 7.648,0 9.966,-0.086L25,13l6,15h2s0.343,-3.055 1,-7c1,-6 0.533,-21 -14,-21z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M30,21c0,2.209 -1.119,4 -2.5,4S25,23.209 25,21s1.119,-4 2.5,-4 2.5,1.791 2.5,4z" + android:fillColor="#FFDC5D"/> + <path + android:pathData="M10,21c-0.552,0 -1,-0.447 -1,-1v-2c0,-0.552 0.448,-1 1,-1s1,0.448 1,1v2c0,0.553 -0.448,1 -1,1zM20,21c-0.553,0 -1,-0.447 -1,-1v-2c0,-0.552 0.447,-1 1,-1s1,0.448 1,1v2c0,0.553 -0.447,1 -1,1z" + android:fillColor="#662113"/> + <path + android:pathData="M16,26h-2c-0.552,0 -1,-0.447 -1,-1s0.448,-1 1,-1h2c0.552,0 1,0.447 1,1s-0.448,1 -1,1z" + android:fillColor="#B7755E"/> + <path + android:pathData="M27,25c0,-2 -2.293,-0.707 -3,0 -1,1 -3,3 -5,2 -2.828,-1.414 -4,-1 -4,-1s-1.171,-0.414 -4,1c-2,1 -4,-1 -5,-2 -0.707,-0.707 -3,-2 -3,0s1,2 1,2c-1,2 1,3 1,3 0,3 3,3 3,3 0,3 4,2 4,2 1,1 3,1 3,1s2,0 3,-1c0,0 4,1 4,-2 0,0 3,0 3,-3 0,0 2,-1 1,-3 0,0 1,0 1,-2z" + android:fillColor="#E6E7E8"/> + <path + android:pathData="M15,28c7,0 4,2 0,2s-7,-2 0,-2z" + android:fillColor="#FFDC5D"/> + <path + android:pathData="M1,14a2,4 0,1 0,4 0a2,4 0,1 0,-4 0z" + android:fillColor="#D1D3D4"/> + <path + android:pathData="M24,14a2,4 0,1 0,4 0a2,4 0,1 0,-4 0z" + android:fillColor="#D1D3D4"/> + <path + android:pathData="M32,29m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#F1F2F2"/> + <path + android:pathData="M29,12c0,1.104 -0.896,2 -2,2H2c-1.104,0 -2,-0.896 -2,-2v-1c0,-1.104 0.896,-2 2,-2h25c1.104,0 2,0.896 2,2v1z" + android:fillColor="#F1F2F2"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml new file mode 100644 index 0000000000000000000000000000000000000000..98e68c20716603f8bdf43fe31ba584a4f621bc00 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M16.806,10.675c-0.92,-2.047 -2.003,-4.066 -3.026,-6.26 -0.028,-0.068 -0.051,-0.138 -0.082,-0.206 -0.064,-0.142 -0.137,-0.277 -0.208,-0.413l-0.052,-0.111 -0.002,0.003C11.798,0.698 8.343,-0.674 5.46,0.621 2.414,1.988 1.164,5.813 2.67,9.163c1.505,3.351 5.194,4.957 8.24,3.589 0.106,-0.047 0.205,-0.105 0.306,-0.159 1.935,0.438 1.994,1.877 1.994,1.877s4.618,-1.521 3.596,-3.795zM4.876,8.173c-0.958,-2.133 -0.252,-4.527 1.575,-5.347 1.826,-0.822 4.084,0.242 5.042,2.374 0.958,2.132 0.253,4.526 -1.573,5.346 -1.828,0.821 -4.087,-0.241 -5.044,-2.373z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M26.978,34.868c1.163,-0.657 2.187,-2.474 1.529,-3.638L16.754,10.559c-1.103,0.496 -2.938,2.313 -3.544,3.912l13.768,20.397z" + android:fillColor="#99AAB5"/> + <path + android:pathData="M30.54,0.62c-2.882,-1.295 -6.338,0.077 -7.976,3.067l-0.003,-0.003 -0.053,0.112c-0.071,0.135 -0.145,0.27 -0.208,0.412 -0.03,0.068 -0.053,0.137 -0.081,0.206 -1.023,2.194 -2.107,4.213 -3.026,6.26 -1.021,2.274 3.597,3.796 3.597,3.796s0.059,-1.439 1.993,-1.877c0.102,0.054 0.2,0.111 0.307,0.159 3.045,1.368 6.733,-0.238 8.24,-3.589 1.505,-3.35 0.255,-7.175 -2.79,-8.543zM31.124,8.173c-0.959,2.132 -3.216,3.194 -5.044,2.373 -1.826,-0.82 -2.531,-3.214 -1.572,-5.346 0.956,-2.132 3.214,-3.195 5.041,-2.374 1.827,0.82 2.532,3.214 1.575,5.347z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M9.022,34.868c-1.163,-0.657 -2.187,-2.474 -1.529,-3.638l11.753,-20.671c1.103,0.496 2.938,2.313 3.544,3.912L9.022,34.868z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M19.562,17.396c0,0.863 -0.701,1.562 -1.562,1.562 -0.863,0 -1.562,-0.699 -1.562,-1.562 0,-0.863 0.699,-1.562 1.562,-1.562 0.862,0 1.562,0.699 1.562,1.562z" + android:fillColor="#99AAB5"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml new file mode 100644 index 0000000000000000000000000000000000000000..087adc8c6da21a450ba490df9b04486e96499298 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M10.515,23.621C10.56,23.8 11.683,28 18,28c6.318,0 7.44,-4.2 7.485,-4.379 0.055,-0.217 -0.043,-0.442 -0.237,-0.554 -0.195,-0.111 -0.439,-0.078 -0.6,0.077C24.629,23.163 22.694,25 18,25s-6.63,-1.837 -6.648,-1.855C11.256,23.05 11.128,23 11,23c-0.084,0 -0.169,0.021 -0.246,0.064 -0.196,0.112 -0.294,0.339 -0.239,0.557z" + android:fillColor="#664500"/> + <path + android:pathData="M9.5,13.5a2.5,3.5 0,1 0,5 0a2.5,3.5 0,1 0,-5 0z" + android:fillColor="#664500"/> + <path + android:pathData="M21.5,13.5a2.5,3.5 0,1 0,5 0a2.5,3.5 0,1 0,-5 0z" + android:fillColor="#664500"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml new file mode 100644 index 0000000000000000000000000000000000000000..0eeb290d9d621ff917f8e53f77481045a071b933 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M22.614,34.845c3.462,-1.154 6.117,-3.034 6.12,-9.373C28.736,21.461 33,17 32.999,12.921 32.998,9 28.384,2.537 17.899,3.635 7.122,4.764 3,8 2.999,15.073c0,4.927 5.304,8.381 8.127,13.518C13,32 18.551,38.187 22.614,34.845z" + android:fillColor="#BE1931"/> + <path + android:pathData="M26.252,3.572c-1.278,-1.044 -3.28,-1.55 -5.35,-1.677 0.273,-0.037 0.542,-0.076 0.82,-0.094 0.973,-0.063 3.614,-1.232 1.4,-1.087 -0.969,0.063 -1.901,0.259 -2.837,0.423 0.237,-0.154 0.479,-0.306 0.74,-0.442C21,0 17,0 14.981,1.688 14.469,1.576 14,1 11,1c-2,0 -4.685,0.926 -3,1 0.917,0.041 2,0 1.858,0.365C9.203,2.425 6,3 6,4c0,0.353 2.76,-0.173 3,0 -1.722,0.644 -3,2 -3,3 0,0.423 2.211,-0.825 3,-1 -1,1 -1.4,1.701 -1.342,2.427 0.038,0.475 2.388,-0.09 2.632,-0.169 0.822,-0.27 3.71,-1.258 4.6,-2.724 0.117,0.285 2.963,1.341 4.11,1.466 0.529,0.058 2.62,0.274 2.141,-0.711C21,6 20,5 19.695,4.025c0.446,-0.019 8.305,0.975 6.557,-0.453z" + android:fillColor="#77B255"/> + <path + android:pathData="M9.339,17.306c-0.136,-1.46 -2.54,-3.252 -2.331,-1 0.136,1.46 2.54,3.252 2.331,1zM16.797,17.859c-0.069,-0.622 -0.282,-1.191 -0.687,-1.671 -0.466,-0.55 -1.075,-0.362 -1.234,0.316 -0.187,0.799 0.082,1.752 0.606,2.372l0.041,0.048c-0.213,-0.525 -0.427,-1.05 -0.642,-1.574l0.006,0.047c0.071,0.64 0.397,1.73 1.136,1.906 0.754,0.182 0.826,-0.988 0.774,-1.444zM22.549,13.018c0.476,-0.955 0.17,-3.962 -0.831,-1.954 -0.476,0.954 -0.171,3.962 0.831,1.954zM29.76,11.561c-0.03,-0.357 -0.073,-0.78 -0.391,-1.01 -1.189,-0.858 -2.381,2.359 -1.385,3.08 0.02,0.012 0.036,0.025 0.055,0.039l-0.331,-0.919c0,0.018 0.001,0.035 0.003,0.052 0.049,0.564 0.376,1.377 1.084,0.948 0.667,-0.406 1.028,-1.444 0.965,-2.19zM28.415,20.128c1.016,-1.569 -0.545,-3.451 -1.78,-1.542 -1.016,1.568 0.546,3.45 1.78,1.542zM22.667,23.022c0.173,-1.938 -2.309,-2.752 -2.51,-0.496 -0.173,1.938 2.309,2.752 2.51,0.496zM12.771,21.81l-0.049,0.004 1.362,0.715c-0.006,-0.004 -0.011,-0.011 -0.018,-0.017 -0.306,-0.28 -1.353,-1.083 -1.788,-0.592 -0.44,0.497 0.498,1.421 0.804,1.703 0.342,0.314 0.928,0.763 1.429,0.73 1.437,-0.093 -0.783,-2.605 -1.74,-2.543zM25.998,27.717c0.969,-1.066 0.725,-4.05 -0.798,-2.376 -0.969,1.066 -0.724,4.05 0.798,2.376zM12.599,13.753c0.093,-0.005 0.187,-0.012 0.28,-0.019 0.703,-0.046 1.004,-1.454 1.042,-1.952 0.044,-0.571 -0.043,-1.456 -0.785,-1.407l-0.281,0.019c-0.702,0.047 -1.004,1.454 -1.042,1.952 -0.044,0.571 0.044,1.457 0.786,1.407zM20.445,29.01c0.395,0.764 0.252,1.623 -0.32,1.919s-1.357,-0.081 -1.753,-0.844c-0.395,-0.764 -0.252,-1.623 0.32,-1.919 0.573,-0.296 1.357,0.081 1.753,0.844z" + android:fillColor="#F4ABBA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml new file mode 100644 index 0000000000000000000000000000000000000000..9761204ab6f290bc3fb767ddac018aa7ec77a163 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M34.956,17.916c0,-0.503 -0.12,-0.975 -0.321,-1.404 -1.341,-4.326 -7.619,-4.01 -16.549,-4.221 -1.493,-0.035 -0.639,-1.798 -0.115,-5.668 0.341,-2.517 -1.282,-6.382 -4.01,-6.382 -4.498,0 -0.171,3.548 -4.148,12.322 -2.125,4.688 -6.875,2.062 -6.875,6.771v10.719c0,1.833 0.18,3.595 2.758,3.885C8.195,34.219 7.633,36 11.238,36h18.044c1.838,0 3.333,-1.496 3.333,-3.334 0,-0.762 -0.267,-1.456 -0.698,-2.018 1.02,-0.571 1.72,-1.649 1.72,-2.899 0,-0.76 -0.266,-1.454 -0.696,-2.015 1.023,-0.57 1.725,-1.649 1.725,-2.901 0,-0.909 -0.368,-1.733 -0.961,-2.336 0.757,-0.611 1.251,-1.535 1.251,-2.581z" + android:fillColor="#FFDB5E"/> + <path + android:pathData="M23.02,21.249h8.604c1.17,0 2.268,-0.626 2.866,-1.633 0.246,-0.415 0.109,-0.952 -0.307,-1.199 -0.415,-0.247 -0.952,-0.108 -1.199,0.307 -0.283,0.479 -0.806,0.775 -1.361,0.775h-8.81c-0.873,0 -1.583,-0.71 -1.583,-1.583s0.71,-1.583 1.583,-1.583H28.7c0.483,0 0.875,-0.392 0.875,-0.875s-0.392,-0.875 -0.875,-0.875h-5.888c-1.838,0 -3.333,1.495 -3.333,3.333 0,1.025 0.475,1.932 1.205,2.544 -0.615,0.605 -0.998,1.445 -0.998,2.373 0,1.028 0.478,1.938 1.212,2.549 -0.611,0.604 -0.99,1.441 -0.99,2.367 0,1.12 0.559,2.108 1.409,2.713 -0.524,0.589 -0.852,1.356 -0.852,2.204 0,1.838 1.495,3.333 3.333,3.333h5.484c1.17,0 2.269,-0.625 2.867,-1.632 0.247,-0.415 0.11,-0.952 -0.305,-1.199 -0.416,-0.245 -0.953,-0.11 -1.199,0.305 -0.285,0.479 -0.808,0.776 -1.363,0.776h-5.484c-0.873,0 -1.583,-0.71 -1.583,-1.583s0.71,-1.583 1.583,-1.583h6.506c1.17,0 2.27,-0.626 2.867,-1.633 0.247,-0.416 0.11,-0.953 -0.305,-1.199 -0.419,-0.251 -0.954,-0.11 -1.199,0.305 -0.289,0.487 -0.799,0.777 -1.363,0.777h-7.063c-0.873,0 -1.583,-0.711 -1.583,-1.584s0.71,-1.583 1.583,-1.583h8.091c1.17,0 2.269,-0.625 2.867,-1.632 0.247,-0.415 0.11,-0.952 -0.305,-1.199 -0.417,-0.246 -0.953,-0.11 -1.199,0.305 -0.289,0.486 -0.799,0.776 -1.363,0.776H23.02c-0.873,0 -1.583,-0.71 -1.583,-1.583s0.709,-1.584 1.583,-1.584z" + android:fillColor="#EE9547"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml new file mode 100644 index 0000000000000000000000000000000000000000..e317ce164232e1715e87cecfc3df4cc3b2fdbea8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml @@ -0,0 +1,72 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M0,34h36v2H0z" + android:fillColor="#939598"/> + <path + android:pathData="M6,27h29v5H6z" + android:fillColor="#231F20"/> + <path + android:pathData="M6.999,32m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" + android:fillColor="#58595B"/> + <path + android:pathData="M12.999,32m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" + android:fillColor="#58595B"/> + <path + android:pathData="M6.999,32m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:fillColor="#A0041E"/> + <path + android:pathData="M12.999,32m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" + android:fillColor="#A0041E"/> + <path + android:pathData="M5,33H1c-1,0 -1.5,-0.5 0,-2l4,-4c1,-1 2,-2.001 2,0v4c0,2 -0.001,2 -2,2z" + android:fillColor="#DD2E44"/> + <path + android:pathData="M8,20c0,3.313 -1.343,6 -3,6s-3,-2.687 -3,-6c0,-3.314 1.343,-6 3,-6s3,2.686 3,6z" + android:fillColor="#231F20"/> + <path + android:pathData="M11,15H7L5,7h8z" + android:fillColor="#6D6E71"/> + <path + android:pathData="M26,25c0,1.104 -0.896,2 -2,2H6c-1.104,0 -2,-0.896 -2,-2V15c0,-1.104 0.896,-2 2,-2h18c1.104,0 2,0.896 2,2v10z" + android:fillColor="#414042"/> + <path + android:pathData="M13,26c0,0.553 -0.448,1 -1,1s-1,-0.447 -1,-1L11,13c0,-0.552 0.448,-1 1,-1s1,0.448 1,1v13zM19,26c0,0.553 -0.447,1 -1,1 -0.553,0 -1,-0.447 -1,-1L17,13c0,-0.552 0.447,-1 1,-1 0.553,0 1,0.448 1,1v13z" + android:fillColor="#C1694F"/> + <path + android:pathData="M36,26c0,0.553 -0.447,1 -1,1H7c-0.552,0 -1,-0.447 -1,-1 0,-0.553 0.448,-1 1,-1h28c0.553,0 1,0.447 1,1z" + android:fillColor="#808285"/> + <path + android:pathData="M29.999,31m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#58595B"/> + <path + android:pathData="M21.999,31m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:fillColor="#58595B"/> + <path + android:pathData="M29.999,31m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#A0041E"/> + <path + android:pathData="M21.999,31m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" + android:fillColor="#A0041E"/> + <path + android:pathData="M12,3H6c-0.552,0 -1,0.448 -1,1v3h8V4c0,-0.552 -0.448,-1 -1,-1z" + android:fillColor="#414042"/> + <path + android:pathData="M23,7h12v18H23z" + android:fillColor="#BE1931"/> + <path + android:pathData="M36,6c0,0.552 -0.447,1 -1,1H23c-0.553,0 -1,-0.448 -1,-1s0.447,-1 1,-1h12c0.553,0 1,0.448 1,1z" + android:fillColor="#A0041E"/> + <path + android:pathData="M25,18h8v5h-8z" + android:fillColor="#EA596E"/> + <path + android:pathData="M30,32h-8c-0.127,0 -0.253,-0.024 -0.371,-0.071L16.807,30H10c-0.552,0 -1,-0.447 -1,-1s0.448,-1 1,-1h7c0.128,0 0.253,0.024 0.372,0.071L22.192,30H30c0.553,0 1,0.447 1,1s-0.447,1 -1,1z" + android:fillColor="#F4900C"/> + <path + android:pathData="M33,10c0,-0.552 -0.447,-1 -1,-1h-6c-0.553,0 -1,0.448 -1,1v5c0,0.552 0.447,1 1,1h6c0.553,0 1,-0.448 1,-1v-5z" + android:fillColor="#55ACEE"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml new file mode 100644 index 0000000000000000000000000000000000000000..c5acc19a72626cb6511a536c40dd9400a8bf23c6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml @@ -0,0 +1,54 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M22,33c0,2.209 -1.791,3 -4,3s-4,-0.791 -4,-3l1,-9c0,-2.209 0.791,-2 3,-2s3,-0.209 3,2l1,9z" + android:fillColor="#662113"/> + <path + android:pathData="M34,17c0,8.837 -7.163,12 -16,12 -8.836,0 -16,-3.163 -16,-12C2,8.164 11,0 18,0s16,8.164 16,17z" + android:fillColor="#5C913B"/> + <path + android:pathData="M4,21a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M28,21a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M8,25a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M12,22a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M8,16a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M5,12a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M27,12a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M12,10a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M20,10a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M24,16a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M16,17a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M20,22a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M16,26a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> + <path + android:pathData="M24,25a2,1 0,1 0,4 0a2,1 0,1 0,-4 0z" + android:fillColor="#3E721D"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml new file mode 100644 index 0000000000000000000000000000000000000000..631da7320dd4302f8e68e648ab9c9141c18acc12 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M5.123,5h6C12.227,5 13,4.896 13,6L13,4c0,-1.104 -0.773,-2 -1.877,-2h-8c-2,0 -3.583,2.125 -3,5 0,0 1.791,9.375 1.917,9.958C2.373,18.5 4.164,20 6.081,20h6.958c1.105,0 -0.039,-1.896 -0.039,-3v-2c0,1.104 -0.773,2 -1.877,2h-4c-1.104,0 -1.833,-1.042 -2,-2S3.539,7.667 3.539,7.667C3.206,5.75 4.018,5 5.123,5zM30.935,5h-6C23.831,5 22,4.896 22,6L22,4c0,-1.104 1.831,-2 2.935,-2h8c2,0 3.584,2.125 3,5 0,0 -1.633,9.419 -1.771,10 -0.354,1.5 -2.042,3 -4,3h-7.146C21.914,20 22,18.104 22,17v-2c0,1.104 1.831,2 2.935,2h4c1.104,0 1.834,-1.042 2,-2s1.584,-7.333 1.584,-7.333C32.851,5.75 32.04,5 30.935,5zM20.832,22c0,-6.958 -2.709,0 -2.709,0s-3,-6.958 -3,0 -3.291,10 -3.291,10h12.292c-0.001,0 -3.292,-3.042 -3.292,-10z" + android:fillColor="#FFAC33"/> + <path + android:pathData="M29.123,6.577c0,6.775 -6.77,18.192 -11,18.192 -4.231,0 -11,-11.417 -11,-18.192 0,-5.195 1,-6.319 3,-6.319 1.374,0 6.025,-0.027 8,-0.027l7,-0.001c2.917,-0.001 4,0.684 4,6.347z" + android:fillColor="#FFCC4D"/> + <path + android:pathData="M27,33c0,1.104 0.227,2 -0.877,2h-16C9.018,35 9,34.104 9,33v-1c0,-1.104 1.164,-2 2.206,-2h13.917c1.042,0 1.877,0.896 1.877,2v1z" + android:fillColor="#C1694F"/> + <path + android:pathData="M29,34.625c0,0.76 0.165,1.375 -1.252,1.375H8.498C7.206,36 7,35.385 7,34.625v-0.25C7,33.615 7.738,33 8.498,33h19.25c0.759,0 1.252,0.615 1.252,1.375v0.25z" + android:fillColor="#C1694F"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml new file mode 100644 index 0000000000000000000000000000000000000000..84f95a8592ddb4d38ae4ae54ddff37822a72b291 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M5.622,33.051l-2.674,-2.673L23.337,9.987c3.344,-3.343 1.337,-8.021 2.007,-8.689 0.666,-0.67 1.335,-0.002 1.335,-0.002l8.023,8.023c0.668,0.668 0,1.336 0,1.336 -0.669,0.67 -5.778,-0.908 -8.692,2.006L5.622,33.051z" + android:fillColor="#FCAB40"/> + <path + android:pathData="M5.457,33.891c0.925,-0.925 0.925,-2.424 0,-3.35 -0.924,-0.924 -2.424,-0.924 -3.349,0l0.087,0.087c-0.371,-0.334 -0.938,-0.331 -1.296,0.027 -0.369,0.368 -0.369,0.968 0,1.336L4.008,35.1c0.37,0.369 0.968,0.369 1.337,0 0.355,-0.356 0.36,-0.919 0.032,-1.29l0.08,0.081z" + android:fillColor="#CCD6DD"/> + <path + android:pathData="M13.31,33.709c-1.516,0 -2.939,-0.59 -4.011,-1.661 -1.071,-1.07 -1.661,-2.495 -1.661,-4.011 0,-1.515 0.59,-2.939 1.661,-4.011L19.995,13.33c1.071,-1.071 2.496,-1.661 4.012,-1.661 1.515,0 2.94,0.59 4.011,1.661 2.211,2.212 2.211,5.811 0,8.022L17.322,32.047c-1.072,1.071 -2.496,1.662 -4.012,1.662zM24.007,15.45c-0.506,0 -0.98,0.197 -1.338,0.554L11.974,26.7c-0.357,0.357 -0.554,0.832 -0.554,1.337 0,0.506 0.197,0.979 0.553,1.336 0.358,0.357 0.832,0.555 1.337,0.555s0.98,-0.197 1.337,-0.555l10.696,-10.695c0.737,-0.737 0.736,-1.937 -0.001,-2.674 -0.356,-0.357 -0.83,-0.554 -1.335,-0.554z" + android:fillColor="#FCAB40"/> + <path + android:pathData="M25.344,24.026c0.736,0.738 1.936,0.738 2.674,0 0.738,-0.739 0.738,-1.937 0,-2.674l-8.022,-8.023c-0.739,-0.738 -1.935,-0.738 -2.673,0 -0.739,0.739 -0.739,1.937 0,2.675l8.021,8.022zM21.332,28.037c0.738,0.738 1.937,0.738 2.674,0 0.738,-0.739 0.738,-1.936 0.002,-2.674l-8.023,-8.023c-0.739,-0.738 -1.936,-0.738 -2.675,0 -0.738,0.738 -0.738,1.936 0,2.675l8.022,8.022zM17.322,32.048c0.738,0.738 1.934,0.738 2.673,0 0.738,-0.738 0.738,-1.937 0,-2.674l-8.021,-8.022c-0.739,-0.738 -1.936,-0.738 -2.675,0 -0.738,0.737 -0.738,1.935 0,2.674l8.023,8.022z" + android:fillColor="#FCAB40"/> + <path + android:pathData="M14.648,13.329c0.369,0.369 0.968,0.369 1.337,0l1.337,-1.336c0.369,-0.369 0.369,-0.968 0,-1.338 -0.37,-0.369 -0.968,-0.369 -1.337,0l-1.337,1.338c-0.37,0.369 -0.37,0.967 0,1.336zM10.637,17.341c0.37,0.371 0.967,0.37 1.337,0l1.336,-1.337c0.37,-0.37 0.371,-0.967 0,-1.337 -0.369,-0.37 -0.967,-0.37 -1.337,0l-1.337,1.337c-0.369,0.37 -0.369,0.968 0.001,1.337zM6.625,21.353c0.37,0.37 0.967,0.37 1.337,0l1.337,-1.338c0.37,-0.369 0.37,-0.967 0,-1.337 -0.369,-0.369 -0.967,-0.369 -1.336,0l-1.337,1.338c-0.37,0.369 -0.37,0.967 -0.001,1.337z" + android:fillColor="#CCD6DD"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml new file mode 100644 index 0000000000000000000000000000000000000000..1cedc1b6ade26eabf29fe58a4ce8676a88d16535 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M9.842,19.922c0,9.842 6.575,9.673 5.158,10.078 -7,2 -8.803,-7.618 -9.464,-7.618 -2.378,0 -5.536,-0.423 -5.536,-2.46C0,17.883 2.46,15 6.151,15c2.379,0 3.691,2.883 3.691,4.922zM36,28.638c0,1.104 -3.518,-0.741 -5,0 -2,1 -2,-0.896 -2,-2s1.343,-1 3,-1 4,1.895 4,3z" + android:fillColor="#77B255"/> + <path + android:pathData="M16.715,33.143c0,2.761 -1.279,2.857 -2.857,2.857S11,35.903 11,33.143c0,-0.489 0.085,-1.029 0.234,-1.587 0.69,-2.59 2.754,-5.556 4.052,-5.556 1.578,0 1.429,4.382 1.429,7.143zM25.286,33.143c0,2.761 1.278,2.857 2.856,2.857C29.721,36 31,35.903 31,33.143c0,-0.489 -0.085,-1.029 -0.234,-1.587 -0.691,-2.59 -2.754,-5.556 -4.052,-5.556 -1.578,0 -1.428,4.382 -1.428,7.143z" + android:fillColor="#77B255"/> + <path + android:pathData="M32,27c0,4 -5.149,4 -11.5,4S9,31 9,27c0,-6.627 5.149,-12 11.5,-12S32,20.373 32,27z" + android:fillColor="#3E721D"/> + <path + android:pathData="M5,18m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" + android:fillColor="#292F33"/> + <path + android:pathData="M23.667,25.1c0,3.591 -1.418,3.9 -3.167,3.9s-3.167,-0.31 -3.167,-3.9S18.75,17 20.5,17s3.167,4.51 3.167,8.1zM30,24c0.871,3.482 -0.784,4 -2.533,4 -1.749,0 -2.533,0.69 -2.533,-2.9s-1.116,-6.5 0.633,-6.5C27.315,18.6 29,20 30,24zM16.067,25.1c0,3.591 -0.785,2.9 -2.534,2.9s-3.404,-0.518 -2.533,-4c1,-4 3.251,-5.4 5,-5.4 1.75,0 0.067,2.91 0.067,6.5z" + android:fillColor="#5C913B"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac1267cd3befe4d4285ec81e0ea566626f0a5b52 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M9,28.5c0,-0.828 0.672,-1.5 1.5,-1.5s1.5,0.672 1.5,1.5v0.5s0,3 3,3 3,-3 3,-3V3.5c0,-0.829 0.671,-1.5 1.5,-1.5s1.5,0.671 1.5,1.5V29s0,6 -6,6 -6,-6 -6,-6v-0.5z" + android:fillColor="#66757F"/> + <path + android:pathData="M19.5,4C28.612,4 36,9.82 36,17c0,0 0,2 -1,2s-3,-2 -3,-2H7s-2,2 -3,2 -1,-2 -1,-2C3,9.82 10.387,4 19.5,4z" + android:fillColor="#744EAA"/> + <path + android:pathData="M19.5,4C26.403,4 32,9.82 32,17c0,0 0,2 -2,2s-5,-2 -5,-2H14s-3,2 -5,2 -2,-2 -2,-2C7,9.82 12.596,4 19.5,4z" + android:fillColor="#9266CC"/> + <path + android:pathData="M19.5,4C23.09,4 25,9.82 25,17c0,0 -3,2 -5,2h-1c-2,0 -5,-2 -5,-2 0,-7.18 1.91,-13 5.5,-13z" + android:fillColor="#744EAA"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml new file mode 100644 index 0000000000000000000000000000000000000000..19cef5d3399d6a2c27538dbcd7f9be87e6c5756e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M36,19.854C33.518,9.923 25.006,1.909 16.031,6.832c0,0 -4.522,-1.496 -5.174,-1.948 -0.635,-0.44 -1.635,-0.904 -0.912,0.436 0.423,0.782 0.875,1.672 2.403,3.317C8,12.958 9.279,18.262 7.743,21.75c-1.304,2.962 -2.577,4.733 -1.31,6.976 1.317,2.33 4.729,3.462 7.018,1.06 1.244,-1.307 0.471,-1.937 3.132,-4.202 2.723,-0.543 4.394,-1.791 4.394,-4.375 0,0 0.795,-0.382 1.826,6.009 0.456,2.818 -0.157,5.632 -0.039,8.783H36V19.854z" + android:fillColor="#C1CDD5"/> + <path + android:pathData="M31.906,6.062c0.531,1.312 0.848,3.71 0.595,5.318 -0.15,-3.923 -3.188,-6.581 -4.376,-7.193 -2.202,-1.137 -4.372,-0.979 -6.799,-0.772 0.111,0.168 0.403,0.814 0.32,1.547 -0.479,-0.875 -1.604,-1.42 -2.333,-1.271 -1.36,0.277 -2.561,0.677 -3.475,1.156 -0.504,0.102 -1.249,0.413 -2.372,1.101 -1.911,1.171 -4.175,4.338 -6.737,3.511 1.042,2.5 3.631,1.845 3.631,1.845 1.207,-1.95 4.067,-3.779 6.168,-4.452 7.619,-1.745 12.614,3.439 15.431,9.398 0.768,1.625 2.611,7.132 4.041,10.292V10.956c-0.749,-1.038 -1.281,-3.018 -4.094,-4.894z" + android:fillColor="#60379A"/> + <path + android:pathData="M13.789,3.662c0.573,0.788 3.236,0.794 4.596,3.82 1.359,3.026 -1.943,2.63 -3.14,1.23 -1.334,-1.561 -1.931,-2.863 -2.165,-3.992 -0.124,-0.596 -0.451,-2.649 0.709,-1.058z" + android:fillColor="#C1CDD5"/> + <path + android:pathData="M14.209,4.962c0.956,0.573 2.164,1.515 2.517,2.596 0.351,1.081 -0.707,0.891 -1.349,-0.042 -0.641,-0.934 -0.94,-1.975 -1.285,-2.263 -0.346,-0.289 0.117,-0.291 0.117,-0.291z" + android:fillColor="#758795"/> + <path + android:pathData="M15.255,14.565m-0.946,0a0.946,0.946 0,1 1,1.892 0a0.946,0.946 0,1 1,-1.892 0" + android:fillColor="#292F33"/> + <path + android:pathData="M8.63,26.877c0.119,0.658 -0.181,1.263 -0.67,1.351 -0.49,0.089 -0.984,-0.372 -1.104,-1.03 -0.119,-0.659 0.182,-1.265 0.671,-1.354 0.49,-0.088 0.984,0.373 1.103,1.033z" + android:fillColor="#53626C"/> + <path + android:pathData="M13.844,8.124l0.003,-0.002 -0.005,-0.007 -0.016,-0.014c-0.008,-0.007 -0.011,-0.019 -0.019,-0.025 -0.009,-0.007 -0.021,-0.011 -0.031,-0.018C12.621,7.078 0.933,-0.495 0.219,0.219 -0.51,0.948 10.443,9.742 11.149,10.28l0.011,0.006 0.541,0.439c0.008,0.007 0.01,0.018 0.018,0.024 0.013,0.01 0.028,0.015 0.042,0.024l0.047,0.038 -0.009,-0.016c0.565,0.361 1.427,0.114 1.979,-0.592 0.559,-0.715 0.577,-1.625 0.066,-2.079z" + android:fillColor="#EE7C0E"/> + <path + android:pathData="M4.677,2.25l0.009,-0.025c-0.301,-0.174 -0.594,-0.341 -0.878,-0.5 -0.016,0.038 -0.022,0.069 -0.041,0.11 -0.112,0.243 -0.256,0.484 -0.429,0.716 -0.166,0.224 -0.349,0.424 -0.541,0.595 -0.02,0.018 -0.036,0.026 -0.056,0.043 0.238,0.22 0.489,0.446 0.745,0.676 0.234,-0.21 0.456,-0.449 0.654,-0.717 0.214,-0.287 0.395,-0.589 0.537,-0.898zM6.952,5.195c0.306,-0.41 0.521,-0.822 0.66,-1.212 -0.292,-0.181 -0.584,-0.36 -0.876,-0.538 -0.076,0.298 -0.247,0.699 -0.586,1.152 -0.31,0.417 -0.613,0.681 -0.864,0.845 0.259,0.223 0.52,0.445 0.779,0.665 0.314,-0.244 0.619,-0.552 0.887,-0.912zM9.87,7.32c0.365,-0.49 0.609,-0.983 0.734,-1.437l-0.906,-0.586c-0.023,0.296 -0.172,0.81 -0.631,1.425 -0.412,0.554 -0.821,0.847 -1.1,0.978l0.814,0.671c0.381,-0.256 0.761,-0.611 1.089,-1.051z" + android:fillColor="#C43512"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml new file mode 100644 index 0000000000000000000000000000000000000000..ba3c4313a336803f7e1fade774e69cd5a1f7302f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M27.989,19.977c-0.622,0 -1.225,0.078 -1.806,0.213L15.811,9.818c0.134,-0.581 0.212,-1.184 0.212,-1.806C16.023,3.587 12.436,0 8.012,0 7.11,0 5.91,0.916 6.909,1.915l2.997,2.997s0.999,1.998 -0.999,3.995 -3.996,0.998 -3.996,0.998L1.915,6.909C0.916,5.91 0,7.105 0,8.012c0,4.425 3.587,8.012 8.012,8.012 0.622,0 1.225,-0.078 1.806,-0.212l10.371,10.371c-0.135,0.581 -0.213,1.184 -0.213,1.806 0,4.425 3.588,8.011 8.012,8.011 0.901,0 2.101,-0.916 1.102,-1.915l-2.997,-2.997s-0.999,-1.998 0.999,-3.995 3.995,-0.999 3.995,-0.999l2.997,2.997c1,0.999 1.916,-0.196 1.916,-1.102 0,-4.425 -3.587,-8.012 -8.011,-8.012z" + android:fillColor="#8899A6"/> +</vector> diff --git a/matrix-sdk-android/src/main/res/values-ar/strings.xml b/matrix-sdk-android/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9aba1721afa64cf350740046a1b170d7e40980c --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ar/strings.xml @@ -0,0 +1,82 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_user_sent_image">أرسل â¨%1$s⩠صورة.</string> + + <string name="notice_room_invite_no_invitee">دعوة من â¨%sâ©</string> + <string name="notice_room_invite">دعى â¨%1$sâ© â¨%2$sâ©</string> + <string name="notice_room_invite_you">دعاك â¨%1$sâ©</string> + <string name="notice_room_join">انضمّ â¨%1$sâ©</string> + <string name="notice_room_leave">غادر â¨%1$sâ©</string> + <string name="notice_room_reject">رÙض â¨%1$s⩠الدعوة</string> + <string name="notice_room_kick">طرد â¨%1$sâ© â¨%2$sâ©</string> + <string name="notice_room_unban">رÙع â¨%1$s⩠الØظر عن â¨%2$sâ©</string> + <string name="notice_room_ban">منع â¨%1$sâ© â¨%2$sâ©</string> + <string name="notice_avatar_url_changed">غيّر â¨%1$s⩠صورته</string> + <string name="notice_display_name_set">ضبط â¨%1$s⩠اسم العرض على â¨%2$sâ©</string> + <string name="notice_display_name_changed_from">غيّر â¨%1$s⩠اسم الØساب المعروض من %2$s⩠إلى %3$sâ©</string> + <string name="notice_display_name_removed">أزال â¨%1$s⩠اسم الØساب المعروض (â¨%2$sâ©)</string> + <string name="notice_room_topic_changed">غيّر â¨%1$s⩠الموضوع إلى: â¨%2$sâ©</string> + <string name="notice_room_name_changed">غيّر â¨%1$s⩠اسم الغرÙØ© إلى: â¨%2$sâ©</string> + <string name="notice_answered_call">ردّ â¨%s⩠على المكالمة.</string> + <string name="notice_ended_call">أنهى â¨%s⩠المكالمة.</string> + <string name="notice_made_future_room_visibility">جعل â¨%1$s⩠تأريخ الغرÙØ© مستقبلًا ظاهرا على %2$s</string> + <string name="notice_room_visibility_invited">كل أعضاء الغرÙØ© من Ù„Øظة دعوتهم.</string> + <string name="notice_room_visibility_joined">كل أعضاء الغرÙØ© من Ù„Øظة انضمامهم.</string> + <string name="notice_room_visibility_shared">كل أعضاء الغرÙØ©.</string> + <string name="notice_room_visibility_world_readable">الكل.</string> + <string name="notice_room_visibility_unknown">المجهول (â¨%sâ©).</string> + <string name="notice_end_to_end">Ùعّل â¨%1$s⩠تعمية الطرÙين (â¨%2$sâ©)</string> + + <string name="notice_requested_voip_conference">طلب â¨%1$s⩠اجتماع VoIP</string> + <string name="notice_voip_started">بدأ اجتماع VoIP</string> + <string name="notice_voip_finished">انتهى اجتماع VoIP</string> + + <string name="notice_room_name_removed">أزال â¨%1$s⩠اسم الغرÙØ©</string> + <string name="notice_room_topic_removed">أزال â¨%1$s⩠موضوع الغرÙØ©</string> + <string name="notice_profile_change_redacted">Øدّث â¨%1$s⩠اللاØØ© â¨%2$sâ©</string> + <string name="notice_room_third_party_invite">أرسل â¨%1$s⩠دعوة إلى â¨%2$s⩠للانضمام إلى الغرÙØ©</string> + <string name="notice_crypto_unable_to_decrypt">** تعذّر ÙÙƒ التعمية: â¨%sâ© **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">لم ÙŠÙرسل جهاز المرسل Ù…ÙØ§ØªÙŠØ Ù‡Ø°Ù‡ الرسالة.</string> + + <string name="unable_to_send_message">تعذّر إرسال الرسالة</string> + + <string name="message_failed_to_upload">Ùشل رÙع الصورة</string> + + <string name="network_error">خطأ ÙÙŠ الشبكة</string> + <string name="matrix_error">خطأ ÙÙŠ «ماترÙكس»</string> + + <string name="room_error_join_failed_empty_room">ليس ممكنا الانضمام ثانيةً إلى غرÙØ© Ùارغة.</string> + + <string name="encrypted_message">رسالة معمّاة</string> + + <string name="medium_email">عنوان البريد الإلكتروني</string> + <string name="medium_phone_number">رقم الهاتÙ</string> + + <string name="summary_message">â€â€â¨%1$sâ©: â€â¨%2$sâ©</string> + <string name="notice_room_withdraw">انسØب â¨%1$s⩠من الدعوة â¨%2$sâ©</string> + <string name="notice_placed_video_call">أجرى â¨%s⩠مكالمة مرئية.</string> + <string name="notice_placed_voice_call">أجرى â¨%s⩠مكالمة صوتية.</string> + <string name="notice_room_third_party_registered_invite">قبل â¨%1$s⩠دعوة â¨%2$sâ©</string> + + <string name="could_not_redact">تعذر التهذيب</string> + <string name="summary_user_sent_sticker">أرسل â¨%1$s⩠ملصقا.</string> + + <string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string> + + <string name="room_displayname_invite_from">دعوة من â¨%sâ©</string> + <string name="room_displayname_empty_room">غرÙØ© Ùارغة</string> + + <string name="room_displayname_two_members">â€â¨%1$sâ© Ùˆ â¨%2$sâ©</string> + <string name="room_displayname_room_invite">دعوة إلى غرÙØ©</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="zero">صÙر</item> + <item quantity="one">واØد</item> + <item quantity="two">اثنان</item> + <item quantity="few">قليل</item> + <item quantity="many">كثير</item> + <item quantity="other">اخرى</item> + </plurals> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-az/strings.xml b/matrix-sdk-android/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c60dfafa7ccc71510a652bd7cf8e82cf35690a8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-az/strings.xml @@ -0,0 +1,182 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s ÅŸÉ™kil göndÉ™rdi.</string> + <string name="summary_user_sent_sticker">%1$s stiker göndÉ™rdi.</string> + + <string name="notice_room_invite_no_invitee">%s-nin dÉ™vÉ™ti</string> + <string name="notice_room_invite">%1$s dÉ™vÉ™t etdi %2$s</string> + <string name="notice_room_invite_you">%1$s sizi dÉ™vÉ™t etdi</string> + <string name="notice_room_join">%1$s qoÅŸuldu</string> + <string name="notice_room_leave">%1$s qalıb</string> + <string name="notice_room_reject">%1$s dÉ™vÉ™ti rÉ™dd etdi</string> + <string name="notice_room_kick">%1$s %2$s-i xaric etdi</string> + <string name="notice_room_unban">%1$s %2$s-i blokdan açdı</string> + <string name="notice_room_ban">%1$s %2$s-i blokladı</string> + <string name="notice_room_withdraw">%1$s %2$s-in dÉ™vÉ™tini geri götürdü</string> + <string name="notice_avatar_url_changed">%1$s avatarı dÉ™yiÅŸdi</string> + <string name="notice_display_name_set">%1$s ekran adını %2$s olaraq tÉ™yin etdi</string> + <string name="notice_display_name_changed_from">%1$s ekran adını %2$s-dan %3$s-ya dÉ™yiÅŸdi</string> + <string name="notice_display_name_removed">%1$s onların göstÉ™rilÉ™n adlarını sildi (%2$s)</string> + <string name="notice_room_topic_changed">%1$s mövzunu dÉ™yiÅŸdi: %2$s</string> + <string name="notice_room_name_changed">%1$s otaq adını dÉ™yiÅŸdirdi: %2$s</string> + <string name="notice_placed_video_call">%s video zÉ™ng etdi.</string> + <string name="notice_placed_voice_call">%s sÉ™sli zÉ™ng etdi.</string> + <string name="notice_answered_call">%s zÉ™ngÉ™ cavab verdi.</string> + <string name="notice_ended_call">%s zÉ™ng baÅŸa çatdı.</string> + <string name="notice_made_future_room_visibility">"%1$s gÉ™lÉ™cÉ™k otaq tarixçəsini %2$s-É™ görünÉ™n etdi"</string> + <string name="notice_room_visibility_invited">bütün otaq üzvlÉ™ri, dÉ™vÉ™t olunduÄŸu andan.</string> + <string name="notice_room_visibility_joined">bütün otaq üzvlÉ™ri, qoÅŸulduÄŸu andan.</string> + <string name="notice_room_visibility_shared">bütün otaq üzvlÉ™ri.</string> + <string name="notice_room_visibility_world_readable">hÉ™r kÉ™s.</string> + <string name="notice_room_visibility_unknown">namÉ™lum (%s).</string> + <string name="notice_end_to_end">%1$s sondan-sona ÅŸifrÉ™lÉ™mÉ™ açdı (%2$s)</string> + <string name="notice_room_update">%s bu otağı tÉ™kmilləşdirdi.</string> + + <string name="notice_requested_voip_conference">%1$s VoIP konfrans istÉ™di</string> + <string name="notice_voip_started">VoIP konfransı baÅŸladı</string> + <string name="notice_voip_finished">VoIP konfransı baÅŸa çatdı</string> + + <string name="notice_avatar_changed_too">(avatar da dÉ™yiÅŸdirilib)</string> + <string name="notice_room_name_removed">%1$s otaq adını sildi</string> + <string name="notice_room_topic_removed">%1$s otaq mövzusunu sildi</string> + <string name="notice_event_redacted">Mesaj silindi</string> + <string name="notice_event_redacted_by">Mesaj %1$s tÉ™rÉ™findÉ™n silindi</string> + <string name="notice_event_redacted_with_reason">Mesaj silindi [sÉ™bÉ™b: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mesaj %1$s tÉ™rÉ™findÉ™n qaldırıldı [sÉ™bÉ™b: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s profilini %2$s yenilÉ™di</string> + <string name="notice_room_third_party_invite">%1$s otaÄŸa qoÅŸulmaq üçün %2$s dÉ™vÉ™tnamÉ™ göndÉ™rdi</string> + <string name="notice_room_third_party_revoked_invite">%1$s otaÄŸa qoÅŸulmaq üçün %2$s dÉ™vÉ™tini ləğv etdi</string> + <string name="notice_room_third_party_registered_invite">%1$s %2$s üçün dÉ™vÉ™ti qÉ™bul etdi</string> + + <string name="notice_crypto_unable_to_decrypt">** ÅžifrÉ™ni aça bilmir: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">GöndÉ™rÉ™nin cihazı bu mesaj üçün açarları bizÉ™ göndÉ™rmÉ™yib.</string> + + <string name="could_not_redact">RedaktÉ™ etmÉ™k olmur</string> + <string name="unable_to_send_message">Mesaj göndÉ™rmÉ™k olmur</string> + + <string name="message_failed_to_upload">Şəkil yüklÉ™mÉ™k olmur</string> + + <string name="network_error">ŞəbÉ™kÉ™ xÉ™tası</string> + <string name="matrix_error">Matris xÉ™tası</string> + + <string name="room_error_join_failed_empty_room">BoÅŸ bir otaÄŸa yenidÉ™n qoÅŸulmaq hazırda mümkün deyil.</string> + + <string name="encrypted_message">ÅžifrÉ™li mesaj</string> + + <string name="medium_email">Elektron poçt ünvanı</string> + <string name="medium_phone_number">Telefon nömrÉ™si</string> + + <string name="room_displayname_invite_from">%s-dÉ™n dÉ™vÉ™t</string> + <string name="room_displayname_room_invite">OtaÄŸa dÉ™vÉ™t</string> + + <string name="room_displayname_two_members">%1$s vÉ™ %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s vÉ™ 1 digÉ™r</item> + <item quantity="other">%1$s vÉ™ %2$d digÉ™rlÉ™ri</item> + </plurals> + + <string name="room_displayname_empty_room">BoÅŸ otaq</string> + + + <string name="verification_emoji_dog">It</string> + <string name="verification_emoji_cat">PiÅŸik</string> + <string name="verification_emoji_lion">Aslan</string> + <string name="verification_emoji_horse">At</string> + <string name="verification_emoji_unicorn">KÉ™rgÉ™dan</string> + <string name="verification_emoji_pig">Donuz</string> + <string name="verification_emoji_elephant">Fil</string> + <string name="verification_emoji_rabbit">DovÅŸan</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Xoruz</string> + <string name="verification_emoji_penguin">Pinqvin</string> + <string name="verification_emoji_turtle">TısbaÄŸa</string> + <string name="verification_emoji_fish">Balıq</string> + <string name="verification_emoji_octopus">Ahtapot</string> + <string name="verification_emoji_butterfly">KÉ™pÉ™nÉ™k</string> + <string name="verification_emoji_flower">Çiçək</string> + <string name="verification_emoji_tree">AÄŸac</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">GöbÉ™lÉ™k</string> + <string name="verification_emoji_globe">Qlobus</string> + <string name="verification_emoji_moon">Ay</string> + <string name="verification_emoji_cloud">Bulud</string> + <string name="verification_emoji_fire">Atəş</string> + <string name="verification_emoji_banana">Banan</string> + <string name="verification_emoji_apple">Alma</string> + <string name="verification_emoji_strawberry">ÇiyÉ™lÉ™k</string> + <string name="verification_emoji_corn">Qarğıdalı</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Tort</string> + <string name="verification_emoji_heart">ÃœrÉ™k</string> + <string name="verification_emoji_smiley">TÉ™bÉ™ssüm</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Papaq</string> + <string name="verification_emoji_glasses">EynÉ™klÉ™r</string> + <string name="verification_emoji_wrench">Açar</string> + <string name="verification_emoji_santa">Santa</string> + <string name="verification_emoji_thumbsup">BaÅŸ barmaqlar yuxarı</string> + <string name="verification_emoji_umbrella">Çətir</string> + <string name="verification_emoji_hourglass">Qum saatı</string> + <string name="verification_emoji_clock">Saat</string> + <string name="verification_emoji_gift">HÉ™diyyÉ™</string> + <string name="verification_emoji_lightbulb">Lampa</string> + <string name="verification_emoji_book">Kitab</string> + <string name="verification_emoji_pencil">QÉ™lÉ™m</string> + <string name="verification_emoji_paperclip">Kağız sancağı</string> + <string name="verification_emoji_scissors">Qayçı</string> + <string name="verification_emoji_lock">Qıfıl</string> + <string name="verification_emoji_key">Açar</string> + <string name="verification_emoji_hammer">Çəkic</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Bayraq</string> + <string name="verification_emoji_train">Qatar</string> + <string name="verification_emoji_bicycle">Velosiped</string> + <string name="verification_emoji_airplane">TÉ™yyarÉ™</string> + <string name="verification_emoji_rocket">Raket</string> + <string name="verification_emoji_trophy">Kubok</string> + <string name="verification_emoji_ball">Top</string> + <string name="verification_emoji_guitar">Gitara</string> + <string name="verification_emoji_trumpet">Saz</string> + <string name="verification_emoji_bell">ZÉ™ng</string> + <string name="verification_emoji_anchor">Anker</string> + <string name="verification_emoji_headphone">Qulaqlıqlar</string> + <string name="verification_emoji_folder">Qovluq</string> + <string name="verification_emoji_pin">Sancaq</string> + + <string name="initial_sync_start_importing_account">Ä°lkin sinxronizasiya: +\nHesab idxal olunur…</string> + <string name="initial_sync_start_importing_account_crypto">Ä°lkin sinxronizasiya: +\nKriptografiyanın idxalı</string> + <string name="initial_sync_start_importing_account_rooms">Ä°lkin sinxronizasiya: +\nOtaqlar idxalı</string> + <string name="initial_sync_start_importing_account_joined_rooms">Ä°lkin sinxronizasiya: +\nOtaqlara daxil olmaq</string> + <string name="initial_sync_start_importing_account_invited_rooms">Ä°lkin sinxronizasiya: +\nDÉ™vÉ™t olunmuÅŸ otaqların idxalı</string> + <string name="initial_sync_start_importing_account_left_rooms">Ä°lkin sinxronizasiya: +\nTÉ™rk olunmuÅŸ otaqların idxalı</string> + <string name="initial_sync_start_importing_account_groups">Ä°lkin sinxronizasiya: +\nÄ°cmaların idxalı</string> + <string name="initial_sync_start_importing_account_data">Ä°lkin sinxronizasiya: +\nHesab mÉ™lumatlarının idxalı</string> + + <string name="event_status_sending_message">Mesaj göndÉ™rilir…</string> + <string name="clear_timeline_send_queue">GöndÉ™rmÉ™ növbÉ™sini tÉ™mizlÉ™yin</string> + + <string name="notice_room_invite_no_invitee_with_reason">%1$s-nin dÉ™vÉ™ti. SÉ™bÉ™b: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s dÉ™vÉ™t olunmuÅŸ %2$s. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s sizi dÉ™vÉ™t etdi. SÉ™bÉ™b: %2$s</string> + <string name="notice_room_join_with_reason">%1$s qoÅŸuldu. SÉ™bÉ™b: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s qalıb. SÉ™bÉ™b: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s dÉ™vÉ™ti rÉ™dd etdi. SÉ™bÉ™b: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s %2$s-i xaric etdi. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s blokdan açdı %2$s. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s blokladı %2$s. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s otaÄŸa qoÅŸulmaq üçün %2$s dÉ™vÉ™tnamÉ™ göndÉ™rdi. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s otaÄŸa qoÅŸulmaq üçün %2$s dÉ™vÉ™tini ləğv etdi. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s %2$s üçün dÉ™vÉ™ti qÉ™bul etdi. SÉ™bÉ™b: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s %2$s dÉ™vÉ™tini geri götürdü. SÉ™bÉ™b: %3$s</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..07d59852f34054e3893d02e2a610816614e3ee64 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -0,0 +1,207 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s изпрати Ñнимка.</string> + + <string name="notice_room_invite_no_invitee">Поканата на %s</string> + <string name="notice_room_invite">%1$s покани %2$s</string> + <string name="notice_room_invite_you">%1$s Ви покани</string> + <string name="notice_room_join">%1$s Ñе приÑъедини в ÑтаÑта</string> + <string name="notice_room_leave">%1$s напуÑна ÑтаÑта</string> + <string name="notice_room_reject">%1$s отхвърли поканата</string> + <string name="notice_room_kick">%1$s изгони %2$s</string> + <string name="notice_room_unban">%1$s отблокира %2$s</string> + <string name="notice_room_ban">%1$s блокира %2$s</string> + <string name="notice_room_withdraw">%1$s оттегли поканата Ñи за %2$s</string> + <string name="notice_avatar_url_changed">%1$s Ñмени ÑвоÑта профилна Ñнимка</string> + <string name="notice_display_name_set">%1$s Ñи Ñложи име %2$s</string> + <string name="notice_display_name_changed_from">%1$s Ñмени Ñвоето име от %2$s на %3$s</string> + <string name="notice_display_name_removed">%1$s премахна Ñвоето име (%2$s)</string> + <string name="notice_room_topic_changed">%1$s Ñмени темата на: %2$s</string> + <string name="notice_room_name_changed">%1$s Ñмени името на ÑтаÑта на: %2$s</string> + <string name="notice_placed_video_call">%s започна видео разговор.</string> + <string name="notice_placed_voice_call">%s започна глаÑов разговор.</string> + <string name="notice_answered_call">%s отговори на повикването.</string> + <string name="notice_ended_call">%s прекрати разговора.</string> + <string name="notice_made_future_room_visibility">%1$s направи бъдещата иÑÑ‚Ð¾Ñ€Ð¸Ñ Ð½Ð° ÑтаÑта видима за %2$s</string> + <string name="notice_room_visibility_invited">вÑички членове, от момента на поканването им в неÑ.</string> + <string name="notice_room_visibility_joined">вÑички членове, от момента на приÑъединÑването им в неÑ.</string> + <string name="notice_room_visibility_shared">вÑички членове в неÑ.</string> + <string name="notice_room_visibility_world_readable">вÑеки.</string> + <string name="notice_room_visibility_unknown">непозната (%s).</string> + <string name="notice_end_to_end">%1$s включи шифроване от край до край (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s заÑви VoIP групов разговор</string> + <string name="notice_voip_started">Започна VoIP групов разговор</string> + <string name="notice_voip_finished">ГруповиÑÑ‚ разговор приключи</string> + + <string name="notice_avatar_changed_too">(профилната Ñнимка Ñъщо беше Ñменена)</string> + <string name="notice_room_name_removed">%1$s премахна името на ÑтаÑта</string> + <string name="notice_room_topic_removed">%1$s премахна темата на ÑтаÑта</string> + <string name="notice_profile_change_redacted">%1$s обнови ÑÐ²Ð¾Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð» %2$s</string> + <string name="notice_room_third_party_invite">%1$s изпрати покана на %2$s да Ñе приÑъедини към ÑтаÑта</string> + <string name="notice_room_third_party_registered_invite">%1$s прие поканата за %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** ÐеуÑпешно разшифроване: %s **</string> + <string name="could_not_redact">ÐеуÑпешно премахване</string> + <string name="unable_to_send_message">ÐеуÑпешно изпращане на Ñъобщението</string> + + <string name="message_failed_to_upload">ÐеуÑпешно качване на Ñнимката</string> + + <string name="network_error">Грешка в мрежата</string> + <string name="matrix_error">Matrix грешка</string> + + <string name="room_error_join_failed_empty_room">Ð’ момента не е възможно да Ñе приÑъедините отново към празна ÑтаÑ.</string> + + <string name="encrypted_message">Шифровано Ñъобщение</string> + + <string name="medium_email">Имейл адреÑ</string> + <string name="medium_phone_number">Телефонен номер</string> + + <string name="notice_crypto_error_unkwown_inbound_session_id">УÑтройÑтвото на Ð¿Ð¾Ð´Ð°Ñ‚ÐµÐ»Ñ Ð½Ðµ изпрати ключовете за това Ñъобщение.</string> + + <string name="summary_user_sent_sticker">%1$s изпрати Ñтикер.</string> + + <string name="room_displayname_invite_from">Покана от %s</string> + <string name="room_displayname_room_invite">Покана за ÑтаÑ</string> + <string name="room_displayname_two_members">%1$s и %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s и 1 друг</item> + <item quantity="other">%1$s и %2$d други</item> + </plurals> + + <string name="room_displayname_empty_room">Празна ÑтаÑ</string> + + <string name="notice_event_redacted">Премахнато Ñъобщение</string> + <string name="notice_event_redacted_by">Съобщение премахнато от %1$s</string> + <string name="notice_event_redacted_with_reason">Премахнато Ñъобщение [причина: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string> + <string name="verification_emoji_dog">Куче</string> + <string name="verification_emoji_cat">Котка</string> + <string name="verification_emoji_lion">Лъв</string> + <string name="verification_emoji_horse">Кон</string> + <string name="verification_emoji_unicorn">Еднорог</string> + <string name="verification_emoji_pig">ПраÑе</string> + <string name="verification_emoji_elephant">Слон</string> + <string name="verification_emoji_rabbit">Заек</string> + <string name="verification_emoji_panda">Панда</string> + <string name="verification_emoji_rooster">Петел</string> + <string name="verification_emoji_penguin">Пингвин</string> + <string name="verification_emoji_turtle">КоÑтенурка</string> + <string name="verification_emoji_fish">Риба</string> + <string name="verification_emoji_octopus">Октопод</string> + <string name="verification_emoji_butterfly">Пеперуда</string> + <string name="verification_emoji_flower">Цвете</string> + <string name="verification_emoji_tree">Дърво</string> + <string name="verification_emoji_cactus">КактуÑ</string> + <string name="verification_emoji_mushroom">Гъба</string> + <string name="verification_emoji_globe">ГлобуÑ</string> + <string name="verification_emoji_moon">Луна</string> + <string name="verification_emoji_cloud">Облак</string> + <string name="verification_emoji_fire">Огън</string> + <string name="verification_emoji_banana">Банан</string> + <string name="verification_emoji_apple">Ябълка</string> + <string name="verification_emoji_strawberry">Ягода</string> + <string name="verification_emoji_corn">Царевица</string> + <string name="verification_emoji_pizza">Пица</string> + <string name="verification_emoji_cake">Торта</string> + <string name="verification_emoji_heart">Сърце</string> + <string name="verification_emoji_smiley">УÑмивка</string> + <string name="verification_emoji_robot">Робот</string> + <string name="verification_emoji_hat">Шапка</string> + <string name="verification_emoji_glasses">Очила</string> + <string name="verification_emoji_wrench">Гаечен ключ</string> + <string name="verification_emoji_santa">ДÑдо Коледа</string> + <string name="verification_emoji_thumbsup">Палец нагоре</string> + <string name="verification_emoji_umbrella">Чадър</string> + <string name="verification_emoji_hourglass">ПÑÑъчен чаÑовник</string> + <string name="verification_emoji_clock">ЧаÑовник</string> + <string name="verification_emoji_gift">Подарък</string> + <string name="verification_emoji_lightbulb">Лампа</string> + <string name="verification_emoji_book">Книга</string> + <string name="verification_emoji_pencil">Молив</string> + <string name="verification_emoji_paperclip">Кламер</string> + <string name="verification_emoji_scissors">Ðожици</string> + <string name="verification_emoji_lock">Катинар</string> + <string name="verification_emoji_key">Ключ</string> + <string name="verification_emoji_hammer">Чук</string> + <string name="verification_emoji_telephone">Телефон</string> + <string name="verification_emoji_flag">Знаме</string> + <string name="verification_emoji_train">Влак</string> + <string name="verification_emoji_bicycle">Колело</string> + <string name="verification_emoji_airplane">Самолет</string> + <string name="verification_emoji_rocket">Ракета</string> + <string name="verification_emoji_trophy">Трофей</string> + <string name="verification_emoji_ball">Топка</string> + <string name="verification_emoji_guitar">Китара</string> + <string name="verification_emoji_trumpet">Тромпет</string> + <string name="verification_emoji_bell">Звънец</string> + <string name="verification_emoji_anchor">Котва</string> + <string name="verification_emoji_headphone">Слушалки</string> + <string name="verification_emoji_folder">Папка</string> + <string name="verification_emoji_pin">Карфица</string> + + <string name="initial_sync_start_importing_account">Ðачална ÑинхронизациÑ: +\nИмпортиране на профил…</string> + <string name="initial_sync_start_importing_account_crypto">Ðачална ÑинхронизациÑ: +\nИмпортиране на данни за шифроване</string> + <string name="initial_sync_start_importing_account_rooms">Ðачална ÑинхронизациÑ: +\nИмпортиране на Ñтаи</string> + <string name="initial_sync_start_importing_account_joined_rooms">Ðачална ÑинхронизациÑ: +\nИмпортиране на Ñтаи, от които Ñъм чаÑÑ‚</string> + <string name="initial_sync_start_importing_account_invited_rooms">Ðачална ÑинхронизациÑ: +\nИмпортиране на Ñтаи, към които Ñъм поканен</string> + <string name="initial_sync_start_importing_account_left_rooms">Ðачална ÑинхронизациÑ: +\nИмпортиране на Ñтаи, които Ñъм напуÑнал</string> + <string name="initial_sync_start_importing_account_groups">Ðачална ÑинхронизациÑ: +\nИмпортиране на общноÑти</string> + <string name="initial_sync_start_importing_account_data">Ðачална ÑинхронизациÑ: +\nИмпортиране на данни за профила</string> + + <string name="notice_room_update">%s обнови тази ÑтаÑ.</string> + + <string name="event_status_sending_message">Изпращане на Ñъобщение…</string> + <string name="clear_timeline_send_queue">ИзчиÑти опашката за изпращане</string> + + <string name="notice_room_third_party_revoked_invite">%1$s оттегли поканата за приÑъединÑване на %2$s към ÑтаÑта</string> + <string name="notice_room_invite_no_invitee_with_reason">поканата на %1$s. Причина: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s покани %2$s. Причина: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s ви покани. Причина: %2$s</string> + <string name="notice_room_join_with_reason">%1$s Ñе приÑъедини в ÑтаÑта. Причина: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s напуÑна ÑтаÑта. Причина: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s отхвърли поканата. Причина: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s изгони %2$s. Причина: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s блокира %2$s. Причина: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s блокира %2$s. Причина: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s изпрати покана до %2$s да Ñе приÑъедини в ÑтаÑта. Причина: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s премахна поканата за приÑъединÑване на %2$s в ÑтаÑта. Причина: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s прие поканата за %2$s. Причина: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s оттегли поканата на %2$s. Причина: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s добави %2$s като Ð°Ð´Ñ€ÐµÑ Ð·Ð° тази ÑтаÑ.</item> + <item quantity="other">%1$s добави %2$s като адреÑи за тази ÑтаÑ.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s премахна %2$s като Ð°Ð´Ñ€ÐµÑ Ð·Ð° тази ÑтаÑ.</item> + <item quantity="other">%1$s премахна %2$s като адреÑи за тази ÑтаÑ.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s добави %2$s и премахна %3$s като адреÑи за тази ÑтаÑ.</string> + + <string name="notice_room_canonical_alias_set">%1$s наÑтрой %2$s като оÑновен Ð°Ð´Ñ€ÐµÑ Ð·Ð° тази ÑтаÑ.</string> + <string name="notice_room_canonical_alias_unset">%1$s премахна оÑÐ½Ð¾Ð²Ð½Ð¸Ñ Ð°Ð´Ñ€ÐµÑ Ð·Ð° тази ÑтаÑ.</string> + + <string name="notice_room_guest_access_can_join">%1$s разреши на гоÑти да Ñе приÑъединÑват в ÑтаÑта.</string> + <string name="notice_room_guest_access_forbidden">%1$s предотврати приÑъединÑването на гоÑти в ÑтаÑта.</string> + + <string name="notice_end_to_end_ok">%1$s включи шифроване от-край-до-край.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s).</string> + + <string name="key_verification_request_fallback_message">%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране поÑредÑтвом чат. Ще Ñ‚Ñ€Ñбва да използвате ÑÑ‚Ð°Ñ€Ð¸Ñ Ð¼ÐµÑ‚Ð¾Ð´ за верифициране на ключове.</string> + + <string name="notice_room_created">%1$s Ñъздаде ÑтаÑта</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..805d13a62ad108ac4fdcdc2f6fa3b5232ae25bbb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml @@ -0,0 +1,295 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_user_sent_image">%1$s à¦à¦•à¦Ÿà¦¿ ফটো পাঠিয়েছে।</string> + <string name="summary_user_sent_sticker">%1$s à¦à¦•à¦Ÿà¦¿ সà§à¦¤à¦¿à¦•à¦¾à¦° পাঠিয়েছে।</string> + + <string name="notice_room_invite_no_invitee">%s à¦à¦° আমনà§à¦¤à§à¦°à¦£</string> + <string name="notice_room_invite">%1$s %2$s কে আমনà§à¦¤à§à¦°à¦£ করেছে</string> + <string name="notice_room_invite_you">%1$s আপনাকে আমনà§à¦¤à§à¦°à¦£ করেছে</string> + <string name="notice_room_join">%1$s রà§à¦® ঠযোগ দিয়েছে</string> + <string name="notice_room_leave">%1$s রà§à¦® ছেড়ে দিয়েছে</string> + <string name="notice_room_reject">%1$s আমনà§à¦¤à§à¦°à¦£ টি বাতিল করেছে</string> + <string name="notice_room_kick">%1$s %2$s কে কিক করেছে</string> + <string name="notice_room_unban">%1$s %2$s কে নিষিদà§à¦§ তালিকা থেকে মà§à¦•à§à¦¤ করেছে</string> + <string name="notice_room_ban">%1$s %2$s কে নিষিদà§à¦§ করেছে</string> + <string name="notice_room_withdraw">%1$s %2$s à¦à¦° আমনà§à¦¤à§à¦°à¦£ ফেরত নিয়েছে</string> + <string name="notice_avatar_url_changed">%1$s নিজের অবতার পরিবরà§à¦¤à¦¨ করেছে</string> + <string name="notice_display_name_set">%1$s নিজের পà§à¦°à¦¦à¦°à§à¦¶à¦¨ নাম %2$s রেখেছে</string> + <string name="notice_display_name_changed_from">%1$s নিজের পà§à¦°à¦¦à¦°à§à¦¶à¦¨ নাম %2$s থেকে %3$s তে পরিবরà§à¦¤à¦¨ করেছে</string> + <string name="notice_display_name_removed">%1$s নিজের পà§à¦°à¦¦à¦°à§à¦¶à¦¨ নাম মà§à¦›à§‡ দিয়েছে (%2$s)</string> + <string name="notice_room_topic_changed">%1$s বিষয় টি à¦à¦¤à§‡ পরিবরà§à¦¤à¦¨ করেছে: %2$s</string> + <string name="notice_room_name_changed">%1$s রà§à¦® à¦à¦° নাম à¦à¦¤à§‡ পরিবরà§à¦¤à¦¨ করেছে: %2$s</string> + <string name="notice_placed_video_call">%s à¦à¦•à¦Ÿà¦¿ à¦à¦¿à¦¡à¦¿à¦“ কল সà§à¦¥à¦¾à¦ªà¦¨ করেছিল।</string> + <string name="notice_placed_voice_call">%s à¦à¦•à¦Ÿà¦¿ à¦à¦¯à¦¼à§‡à¦¸ কল দিয়েছে।</string> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_you_sent_image">আপনি à¦à¦•à¦Ÿà¦¿ ছবি পà§à¦°à§‡à¦°à¦£ করেছেন।</string> + <string name="summary_you_sent_sticker">আপনি à¦à¦•à¦Ÿà¦¿ সà§à¦¤à¦¿à¦•à¦¾à¦° পাঠিয়েছেন।</string> + + <string name="notice_room_invite_no_invitee_by_you">আপনার আমনà§à¦¤à§à¦°à¦£</string> + <string name="notice_room_created">%1$s ককà§à¦·à¦Ÿà¦¿ তৈরি করেছেন</string> + <string name="notice_room_created_by_you">আপনি ককà§à¦·à¦Ÿà¦¿ তৈরি করেছেন</string> + <string name="notice_room_invite_by_you">আপনি %1$s কে আমনà§à¦¤à§à¦°à¦¿à¦¤ করেছেন</string> + <string name="notice_room_join_by_you">আপনি ককà§à¦·à§‡ যোগ দিয়েছেন</string> + <string name="notice_room_leave_by_you">আপনি ককà§à¦· ছেড়ে দিয়েছেন</string> + <string name="notice_room_reject_by_you">আপনি আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ বাতিল করেছেন</string> + <string name="notice_room_kick_by_you">আপনি %1$s কে কীক করেছেন</string> + <string name="notice_room_unban_by_you">আপনি %1$s কে নিষিদà§à¦§ মà§à¦•à§à¦¤ করেছেন</string> + <string name="notice_room_ban_by_you">আপনি %1$s কে নিষিদà§à¦§ করেছেন</string> + <string name="notice_room_withdraw_by_you">আপনি %1$s à¦à¦° আমনà§à¦¤à§à¦°à¦£ পà§à¦°à¦¤à§à¦¯à¦¾à¦¹à¦¾à¦° করেছেন</string> + <string name="notice_avatar_url_changed_by_you">আপনি আপনার অবতারটি পরিবরà§à¦¤à¦¨ করেছেন</string> + <string name="notice_display_name_set_by_you">আপনি আপনার পà§à¦°à¦¦à¦°à§à¦¶à¦¨à§‡à¦° নামটি %1$s তে সেট করেছেন</string> + <string name="notice_display_name_changed_from_by_you">আপনি আপনার পà§à¦°à¦¦à¦°à§à¦¶à¦¨à§‡à¦° নামটি %1$s থেকে %2$s ঠপরিবরà§à¦¤à¦¨ করেছেন</string> + <string name="notice_display_name_removed_by_you">আপনি আপনার পà§à¦°à¦¦à¦°à§à¦¶à¦¨à§‡à¦° নামটি সরিয়ে দিয়েছেন (যেটা ছিল %1$s)</string> + <string name="notice_room_topic_changed_by_you">আপনি বিষয়টিকে à¦à¦¤à§‡ পরিবরà§à¦¤à¦¨ করেছেন: %1$s</string> + <string name="notice_room_avatar_changed">%1$s ককà§à¦·à§‡à¦° অবতারটি পরিবরà§à¦¤à¦¨ করেছে</string> + <string name="notice_room_avatar_changed_by_you">আপনি ককà§à¦·à§‡à¦° অবতারটি পরিবরà§à¦¤à¦¨ করেছেন</string> + <string name="notice_room_name_changed_by_you">আপনি ককà§à¦·à§‡à¦° নাম à¦à¦¤à§‡ পরিবরà§à¦¤à¦¨ করেছেন:%1$s</string> + <string name="notice_placed_video_call_by_you">আপনি à¦à¦•à¦Ÿà¦¿ à¦à¦¿à¦¡à¦¿à¦“ কল করেছেন।</string> + <string name="notice_placed_voice_call_by_you">আপনি à¦à¦•à¦Ÿà¦¿ à¦à¦¯à¦¼à§‡à¦¸ কল দিয়েছেন।</string> + <string name="notice_call_candidates">কল সেটআপ করার জনà§à¦¯ %s ডেটা পà§à¦°à§‡à¦°à¦£ করেছে।</string> + <string name="notice_call_candidates_by_you">আপনি কল সেটআপ করার জনà§à¦¯ ডেটা পà§à¦°à§‡à¦°à¦£ করেছেন।</string> + <string name="notice_answered_call">%s কলটির উতà§à¦¤à¦° দিয়েছে।</string> + <string name="notice_answered_call_by_you">আপনি কলটি উতà§à¦¤à¦° দিয়েছেন।</string> + <string name="notice_ended_call">%s কলটি শেষ করেছেন।</string> + <string name="notice_ended_call_by_you">আপনি কলটি শেষ করেছেন।</string> + <string name="notice_made_future_room_visibility">%1$s à¦à¦¬à¦¿à¦·à§à¦¯à¦¤à§‡à¦° ঘরের ইতিহাস %2$s à¦à¦° কাছে দৃশà§à¦¯à¦®à¦¾à¦¨ করে তà§à¦²à§‡à¦›à§‡</string> + <string name="notice_made_future_room_visibility_by_you">আপনি à¦à¦¬à¦¿à¦·à§à¦¯à¦¤à§‡à¦° ককà§à¦· ইতিহাস %1$s à¦à¦° কাছে দৃশà§à¦¯à¦®à¦¾à¦¨ করেছেন</string> + <string name="notice_room_visibility_invited">ককà§à¦·à§‡à¦° সমসà§à¦¤ সদসà§à¦¯, যখন থেকে তারা আমনà§à¦¤à§à¦°à¦¿à¦¤à¥¤</string> + <string name="notice_room_visibility_joined">ককà§à¦·à§‡à¦° সমসà§à¦¤ সদসà§à¦¯, যখন থেকে তারা যোগদান করেছিল।</string> + <string name="notice_room_visibility_shared">সমসà§à¦¤ ককà§à¦·à§‡à¦° সদসà§à¦¯à¥¤</string> + <string name="notice_room_visibility_world_readable">যে কেউ।</string> + <string name="notice_room_visibility_unknown">অজানা (%s)।</string> + <string name="notice_end_to_end">%1$s à¦à¦¨à§à¦¡-টà§-à¦à¦¨à§à¦¡ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছে (%2$s)</string> + <string name="notice_end_to_end_by_you">আপনি শেষ-থেকে-শেষ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছেন (%1$s)</string> + <string name="notice_room_update">%s à¦à¦‡ ককà§à¦·à¦Ÿà¦¿à¦•à§‡ আপগà§à¦°à§‡à¦¡ করেছে।</string> + <string name="notice_room_update_by_you">আপনি à¦à¦‡ ককà§à¦·à¦Ÿà¦¿ আপগà§à¦°à§‡à¦¡ করেছেন।</string> + + <string name="notice_requested_voip_conference">%1$s à¦à¦•à¦Ÿà¦¿ à¦à¦¿à¦“আইপি সমà§à¦®à§‡à¦²à¦¨à§‡à¦° জনà§à¦¯ অনà§à¦°à§‹à¦§ করেছে</string> + <string name="notice_requested_voip_conference_by_you">আপনি à¦à¦•à¦Ÿà¦¿ à¦à¦¿à¦“আইপি সমà§à¦®à§‡à¦²à¦¨à§‡à¦° অনà§à¦°à§‹à¦§ করেছেন</string> + <string name="notice_voip_started">à¦à¦¿à¦“আইপি সমà§à¦®à§‡à¦²à¦¨ শà§à¦°à§ হয়েছে</string> + <string name="notice_voip_finished">à¦à¦¿à¦“আইপি সমà§à¦®à§‡à¦²à¦¨ শেষ হয়েছে</string> + + <string name="notice_avatar_changed_too">(আবতারটিও পরিবরà§à¦¤à¦¨ করা হয়েছিল)</string> + <string name="notice_room_name_removed">%1$s ককà§à¦·à§‡à¦° নাম সরিয়েছে</string> + <string name="notice_room_name_removed_by_you">আপনি ককà§à¦·à§‡à¦° নাম সরিয়েছেন</string> + <string name="notice_room_topic_removed">%1$s ককà§à¦·à§‡à¦° বিষয় মà§à¦›à§‡ ফেলেছে</string> + <string name="notice_room_topic_removed_by_you">আপনি ককà§à¦·à§‡à¦° বিষয়টিকে সরিয়ে দিয়েছেন</string> + <string name="notice_room_avatar_removed">%1$s ককà§à¦·à§‡à¦° অবতার সরিয়ে নিয়েছে</string> + <string name="notice_room_avatar_removed_by_you">আপনি ককà§à¦·à§‡à¦° অবতার সরিয়েছেন</string> + <string name="notice_event_redacted">বারà§à¦¤à¦¾ সরানো হয়েছে</string> + <string name="notice_event_redacted_by">%1$s দà§à¦¬à¦¾à¦°à¦¾ বারà§à¦¤à¦¾ সরানো হয়েছে</string> + <string name="notice_event_redacted_with_reason">বারà§à¦¤à¦¾ সরানো হয়েছে [কারণ:%1$s]</string> + <string name="notice_event_redacted_by_with_reason">%1$s দà§à¦¬à¦¾à¦°à¦¾ বারà§à¦¤à¦¾ সরানো হয়েছে [কারণ: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s তাদের পà§à¦°à§‹à¦«à¦¾à¦‡à¦² %2$ আপডেট করেছে</string> + <string name="notice_profile_change_redacted_by_you">আপনি আপনার পà§à¦°à§‹à¦«à¦¾à¦‡à¦² %1$s আপডেট করেছেন</string> + <string name="notice_room_third_party_invite">%1$s %2$s কে ঘরে যোগদানের জনà§à¦¯ à¦à¦•à¦Ÿà¦¿ আমনà§à¦¤à§à¦°à¦£ পাঠিয়েছে</string> + <string name="notice_room_third_party_invite_by_you">আপনি %1$s কে ঘরে যোগদানের জনà§à¦¯ à¦à¦•à¦Ÿà¦¿ আমনà§à¦¤à§à¦°à¦£ পà§à¦°à§‡à¦°à¦£ করেছেন</string> + <string name="notice_room_third_party_revoked_invite">%1$s %2$s à¦à¦° ককà§à¦·à§‡ যোগদানের আমনà§à¦¤à§à¦°à¦£ বাতিল করে দিয়েছিল</string> + <string name="notice_room_third_party_revoked_invite_by_you">আপনি %1$s à¦à¦° ককà§à¦·à§‡ যোগদানের জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ বাতিল করেছেন</string> + <string name="notice_room_third_party_registered_invite">%1$s %2$s à¦à¦° জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ গà§à¦°à¦¹à¦£ করেছে</string> + <string name="notice_room_third_party_registered_invite_by_you">আপনি %1$s à¦à¦° জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ গà§à¦°à¦¹à¦£ করেছেন</string> + + <string name="notice_widget_added">%1$s %2$s উইজেট যà§à¦•à§à¦¤ করেছে</string> + <string name="notice_widget_added_by_you">আপনি %1$s উইজেট যà§à¦•à§à¦¤ করেছেন</string> + <string name="notice_widget_removed">%1$s %2$s উইজেট সরিয়ে দিয়েছেন</string> + <string name="notice_widget_removed_by_you">আপনি %1$s উইজেট সরিয়েছেন</string> + <string name="notice_widget_modified">%1$s %2$s উইজেট পরিবরà§à¦¤à¦¨ করেছেন</string> + <string name="notice_widget_modified_by_you">আপনি %1$s উইজেট পরিবরà§à¦¤à¦¨ করেছেন</string> + + <string name="power_level_admin">অà§à¦¯à¦¾à¦¡à¦®à¦¿à¦¨</string> + <string name="power_level_moderator">নিয়ামক</string> + <string name="power_level_default">ডিফলà§à¦Ÿ</string> + <string name="power_level_custom">কাসà§à¦Ÿà¦® (%1$d)</string> + <string name="power_level_custom_no_value">কাসà§à¦Ÿà¦®</string> + + <string name="notice_power_level_changed_by_you">আপনি %1$s à¦à¦° পাওয়ার সà§à¦¤à¦° পরিবরà§à¦¤à¦¨ করেছেন।</string> + <string name="notice_power_level_changed">%1$s %2$s à¦à¦° পাওয়ার সà§à¦¤à¦° পরিবরà§à¦¤à¦¨ করেছে।</string> + <string name="notice_power_level_diff">%1$s %2$s থেকে %3$s পরà§à¦¯à¦¨à§à¦¤</string> + + <string name="notice_crypto_unable_to_decrypt">** ডিকà§à¦°à¦¿à¦ªà§à¦Ÿ করতে অকà§à¦·à¦®: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">পà§à¦°à§‡à¦°à¦•à§‡à¦° ডিà¦à¦¾à¦‡à¦¸ আমাদের à¦à¦‡ বারà§à¦¤à¦¾à¦° জনà§à¦¯ কীগà§à¦²à¦¿ পà§à¦°à§‡à¦°à¦£ করেনি।</string> + + <string name="could_not_redact">পà§à¦¨à¦°à¦¾à¦¯à¦¼ পà§à¦°à¦¤à¦¿à¦•à§à¦°à¦¿à¦¯à¦¼à¦¾ করতে পারেনি</string> + <string name="unable_to_send_message">বারà§à¦¤à¦¾ পাঠাতে অকà§à¦·à¦®</string> + + <string name="message_failed_to_upload">চিতà§à¦° আপলোড করতে বà§à¦¯à¦°à§à¦¥</string> + + <string name="network_error">নেটওয়ারà§à¦• তà§à¦°à§à¦Ÿà¦¿</string> + <string name="matrix_error">মà§à¦¯à¦¾à¦Ÿà§à¦°à¦¿à¦•à§à¦¸ তà§à¦°à§à¦Ÿà¦¿</string> + + <string name="room_error_join_failed_empty_room">খালি ককà§à¦·à§‡ পà§à¦¨à¦°à¦¾à¦¯à¦¼ যোগদান করা বরà§à¦¤à¦®à¦¾à¦¨à§‡ সমà§à¦à¦¬ নয়।</string> + + <string name="encrypted_message">à¦à¦¨à¦•à§à¦°à¦¿à¦ªà§à¦Ÿ করা বারà§à¦¤à¦¾</string> + + <string name="medium_email">ইমেল ঠিকানা</string> + <string name="medium_phone_number">ফোন নমà§à¦¬à¦°</string> + + <string name="room_displayname_invite_from">%s থেকে আমনà§à¦¤à§à¦°à¦£ করà§à¦¨</string> + <string name="room_displayname_room_invite">ককà§à¦· আমনà§à¦¤à§à¦°à¦£</string> + + <string name="room_displayname_two_members">%1$s à¦à¦¬à¦‚ %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s à¦à¦¬à¦‚ অনà§à¦¯ ১ জন</item> + <item quantity="other">%1$s à¦à¦¬à¦‚ অনà§à¦¯à¦¾à¦¨à§à¦¯ %2$d জন</item> + </plurals> + + <string name="room_displayname_empty_room">খালি ককà§à¦·</string> + + + <string name="verification_emoji_dog">কà§à¦•à§à¦°</string> + <string name="verification_emoji_cat">বেড়াল</string> + <string name="verification_emoji_lion">সিংহ</string> + <string name="verification_emoji_horse">ঘোড়া</string> + <string name="verification_emoji_unicorn">ইউনিকরà§à¦¨</string> + <string name="verification_emoji_pig">শূকর</string> + <string name="verification_emoji_elephant">হাতি</string> + <string name="verification_emoji_rabbit">খরগোশ</string> + <string name="verification_emoji_panda">পানà§à¦¡à¦¾</string> + <string name="verification_emoji_rooster">গৃহপালিত মোরগ</string> + <string name="verification_emoji_penguin">পেংগà§à¦‡à¦¨</string> + <string name="verification_emoji_turtle">কচà§à¦›à¦ª</string> + <string name="verification_emoji_fish">মাছ</string> + <string name="verification_emoji_octopus">অকà§à¦Ÿà§‹à¦ªà¦¾à¦¸</string> + <string name="verification_emoji_butterfly">পà§à¦°à¦œà¦¾à¦ªà¦¤à¦¿</string> + <string name="verification_emoji_flower">ফà§à¦²</string> + <string name="verification_emoji_tree">গাছ</string> + <string name="verification_emoji_cactus">ফণীমনসা</string> + <string name="verification_emoji_mushroom">মাশরà§à¦®</string> + <string name="verification_emoji_globe">পৃথিবী</string> + <string name="verification_emoji_moon">চনà§à¦¦à§à¦°</string> + <string name="verification_emoji_cloud">মেঘ</string> + <string name="verification_emoji_fire">আগà§à¦¨</string> + <string name="verification_emoji_banana">কলা</string> + <string name="verification_emoji_apple">আপেল</string> + <string name="verification_emoji_strawberry">সà§à¦Ÿà§à¦°à¦¬à§‡à¦°à¦¿</string> + <string name="verification_emoji_corn">à¦à§‚টà§à¦Ÿà¦¾</string> + <string name="verification_emoji_pizza">পিজা</string> + <string name="verification_emoji_cake">কেক</string> + <string name="verification_emoji_heart">হৃদয়</string> + <string name="verification_emoji_smiley">সà§à¦®à¦¾à¦‡à¦²à¦¿</string> + <string name="verification_emoji_robot">রোবট</string> + <string name="verification_emoji_hat">টà§à¦ªà¦¿</string> + <string name="verification_emoji_glasses">চশমা</string> + <string name="verification_emoji_wrench">রেঞà§à¦š</string> + <string name="verification_emoji_santa">সানà§à¦¤à¦¾</string> + <string name="verification_emoji_thumbsup">থামà§à¦¬à¦¸ আপ</string> + <string name="verification_emoji_umbrella">ছাতা</string> + <string name="verification_emoji_hourglass">বালিঘড়ি</string> + <string name="verification_emoji_clock">ঘড়ি</string> + <string name="verification_emoji_gift">উপহার</string> + <string name="verification_emoji_lightbulb">আলো বালব</string> + <string name="verification_emoji_book">বই</string> + <string name="verification_emoji_pencil">পেনà§à¦¸à¦¿à¦²</string> + <string name="verification_emoji_paperclip">পেপার কà§à¦²à¦¿à¦ª</string> + <string name="verification_emoji_scissors">কাà¦à¦šà¦¿</string> + <string name="verification_emoji_lock">লক</string> + <string name="verification_emoji_key">চাবি</string> + <string name="verification_emoji_hammer">হাতà§à¦¡à¦¼à¦¿</string> + <string name="verification_emoji_telephone">টেলিফোন</string> + <string name="verification_emoji_flag">পতাকা</string> + <string name="verification_emoji_train">রেলগাড়ি</string> + <string name="verification_emoji_bicycle">সাইকেল</string> + <string name="verification_emoji_airplane">বিমান</string> + <string name="verification_emoji_rocket">রকেট</string> + <string name="verification_emoji_trophy">টà§à¦°à¦«à¦¿</string> + <string name="verification_emoji_ball">বল</string> + <string name="verification_emoji_guitar">গিটার</string> + <string name="verification_emoji_trumpet">টà§à¦°à¦¾à¦®à§à¦ªà§‡à¦Ÿ</string> + <string name="verification_emoji_bell">ঘণà§à¦Ÿà¦¾</string> + <string name="verification_emoji_anchor">নোঙà§à¦—র</string> + <string name="verification_emoji_headphone">হেডফোন</string> + <string name="verification_emoji_folder">ফোলà§à¦¡à¦¾à¦°</string> + <string name="verification_emoji_pin">পিন</string> + + <string name="initial_sync_start_importing_account">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nঅà§à¦¯à¦¾à¦•à¦¾à¦‰à¦¨à§à¦Ÿ আমদানি করা হচà§à¦›à§‡â€¦</string> + <string name="initial_sync_start_importing_account_crypto">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nকà§à¦°à¦¿à¦ªà§à¦Ÿà§‹ আমদানি হচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_rooms">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nককà§à¦·à¦—à§à¦²à¦¿ আমদানি করা হচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_joined_rooms">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nযোগ করা ককà§à¦·à¦—à§à¦²à¦¿à¦¤à§‡ আমদানি করা হিচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_invited_rooms">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nআমনà§à¦¤à§à¦°à¦¿à¦¤ করা ককà§à¦·à¦—à§à¦²à¦¿à¦¤à§‡ আমদানি করা হিচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_left_rooms">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nছেড়ে দেওয়া ককà§à¦·à¦—à§à¦²à¦¿à¦¤à§‡ আমদানি করা হিচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_groups">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nসমà§à¦ªà§à¦°à¦¦à¦¾à¦¯à¦¼à¦—à§à¦²à¦¿ আমদানি করা হচà§à¦›à§‡</string> + <string name="initial_sync_start_importing_account_data">পà§à¦°à¦¾à¦¥à¦®à¦¿à¦• সিঙà§à¦•: +\nঅà§à¦¯à¦¾à¦•à¦¾à¦‰à¦¨à§à¦Ÿ ডেটা আমদানি করা হচà§à¦›à§‡</string> + + <string name="event_status_sending_message">বারà§à¦¤à¦¾ পà§à¦°à§‡à¦°à¦£ করা হচà§à¦›à§‡ …</string> + <string name="clear_timeline_send_queue">পà§à¦°à§‡à¦°à¦£ সারি পরিষà§à¦•à¦¾à¦° করà§à¦¨</string> + + <string name="notice_room_invite_no_invitee_with_reason">%1$s à¦à¦° আমনà§à¦¤à§à¦°à¦£à¥¤ কারণ: %2$s</string> + <string name="notice_room_invite_no_invitee_with_reason_by_you">আপনার আমনà§à¦¤à§à¦°à¦£à¥¤ কারণ: %1$s</string> + <string name="notice_room_invite_with_reason">%1$s আমনà§à¦¤à§à¦°à¦¿à¦¤ করেছেন %2$s কে। কারণ: %3$s</string> + <string name="notice_room_invite_with_reason_by_you">আপনি %1$s কে আমনà§à¦¤à§à¦°à¦¿à¦¤ করেছেন। কারণ: %2$s</string> + <string name="notice_room_invite_you_with_reason">%1$s আপনাকে আমনà§à¦¤à§à¦°à¦£ করেছে। কারণ: %2$s</string> + <string name="notice_room_join_with_reason">%1$s রà§à¦® ঠযোগ দিয়েছে। কারণ: %2$s</string> + <string name="notice_room_join_with_reason_by_you">আপনি ককà§à¦·à§‡ যোগ দিয়েছেন। কারণ: %1$s</string> + <string name="notice_room_leave_with_reason">%1$s রà§à¦® ছেড়ে দিয়েছে। কারণ: %2$s</string> + <string name="notice_room_leave_with_reason_by_you">আপনি ককà§à¦· ছেড়ে দিয়েছেন। কারণ: %1$s</string> + <string name="notice_room_reject_with_reason">%1$s আমনà§à¦¤à§à¦°à¦£ বাতিল করেছেন। কারণ: %2$s</string> + <string name="notice_room_reject_with_reason_by_you">আপনি আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ বাতিল করেছেন। কারণ: %1$s</string> + <string name="notice_room_kick_with_reason">%1$s %2$s কে কিক করেছে। কারণ: %2$s</string> + <string name="notice_room_kick_with_reason_by_you">আপনি %1$s কে কীক করেছেন। কারণ: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s %2$s কে নিষিদà§à¦§ তালিকা থেকে মà§à¦•à§à¦¤ করেছে। কারণ: %3$s</string> + <string name="notice_room_unban_with_reason_by_you">আপনি %1$s কে নিষিদà§à¦§ মà§à¦•à§à¦¤ করেছেন। কারণ: %2$s</string> + <string name="notice_room_ban_with_reason">%1$s %2$s কে নিষিদà§à¦§ করেছে। কারণ: %3$s</string> + <string name="notice_room_ban_with_reason_by_you">আপনি %1$s কে নিষিদà§à¦§ করেছেন। কারণ: %2$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s রà§à¦®à§‡à¦° সাথে যোগ দিতে %2$s কে à¦à¦•à¦Ÿà¦¿ আমনà§à¦¤à§à¦°à¦£ পাঠিয়েছেন। কারণ: %3$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">আপনি %1$s কে ঘরে যোগদানের জনà§à¦¯ à¦à¦•à¦Ÿà¦¿ আমনà§à¦¤à§à¦°à¦£ পà§à¦°à§‡à¦°à¦£ করেছেন। কারণ: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s %2$s à¦à¦° ককà§à¦·à§‡ যোগদানের আমনà§à¦¤à§à¦°à¦£ বাতিল করে দিয়েছিল। কারণ: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">আপনি %1$s à¦à¦° ককà§à¦·à§‡ যোগদানের জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ বাতিল করেছেন। কারণ: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s %2$s à¦à¦° জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£ গà§à¦°à¦¹à¦£ করেছেন। কারণ: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">আপনি %1$s à¦à¦° জনà§à¦¯ আমনà§à¦¤à§à¦°à¦£à¦Ÿà¦¿ গà§à¦°à¦¹à¦£ করেছেন। কারণ: %2$s</string> + <string name="notice_room_withdraw_with_reason">%1$s %2$s à¦à¦° আমনà§à¦¤à§à¦°à¦£ ফেরত নিয়েছে। কারণ: %3$s</string> + <string name="notice_room_withdraw_with_reason_by_you">আপনি %1$s à¦à¦° আমনà§à¦¤à§à¦°à¦£ পà§à¦°à¦¤à§à¦¯à¦¾à¦¹à¦¾à¦° করেছেন। কারণ: %2$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s à¦à¦‡ ঘরের ঠিকানা হিসাবে %2$s যà§à¦•à§à¦¤ করেছে।</item> + <item quantity="other">%1$s à¦à¦‡ ঘরের ঠিকানাগà§à¦²à¦¿ হিসাবে %2$s যà§à¦•à§à¦¤ করেছে।</item> + </plurals> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">আপনি à¦à¦‡ ককà§à¦·à§‡à¦° জনà§à¦¯ ঠিকানা হিসাবে %1$s যà§à¦•à§à¦¤ করেছেন।</item> + <item quantity="other">আপনি à¦à¦‡ ককà§à¦·à§‡à¦° ঠিকানা হিসাবে %1$s যà§à¦•à§à¦¤ করেছেন।</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s à¦à¦‡ ঘরের ঠিকানা হিসাবে %2$s সরানো হয়েছে।</item> + <item quantity="other">%1$s %3$s কে à¦à¦‡ ঘরের ঠিকানা হিসাবে সরানো হয়েছে।</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">আপনি à¦à¦‡ ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।</item> + <item quantity="other">আপনি à¦à¦‡ ঘরের ঠিকানা হিসাবে %2$s গà§à¦²à¦¿ সরিয়েছেন।</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s %2$s যোগ করেছে à¦à¦¬à¦‚ %3$s গà§à¦²à¦¿ à¦à¦‡ ঘরের ঠিকানা হিসাবে সরানো হয়েছে।</string> + <string name="notice_room_aliases_added_and_removed_by_you">আপনি %1$s যোগ করেছেন à¦à¦¬à¦‚ %2$s কে à¦à¦‡ ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন।</string> + + <string name="notice_room_canonical_alias_set">%1$s à¦à¦‡ ঘরের মূল ঠিকানাটি %2$s তে সেট করে।</string> + <string name="notice_room_canonical_alias_set_by_you">আপনি à¦à¦‡ ঘরের মূল ঠিকানাটি %1$s তে সেট করেছেন।</string> + <string name="notice_room_canonical_alias_unset">%1$s à¦à¦‡ ঘরের মূল ঠিকানা সরিয়ে নিয়েছে।</string> + <string name="notice_room_canonical_alias_unset_by_you">আপনি à¦à¦‡ ঘরের মূল ঠিকানা সরিয়েছেন।</string> + + <string name="notice_room_guest_access_can_join">%1$s অতিথিদের ঘরে যোগদানের অনà§à¦®à¦¤à¦¿ দিয়েছে।</string> + <string name="notice_room_guest_access_can_join_by_you">আপনি অতিথিদের ঘরে যোগদানের অনà§à¦®à¦¤à¦¿ দিয়েছেন।</string> + <string name="notice_room_guest_access_forbidden">%1$s অতিথিদের ঘরে যোগদান করতে বাধা দিয়েছে।</string> + <string name="notice_room_guest_access_forbidden_by_you">আপনি অতিথিদের ঘরে যোগদান করতে বাধা দিয়েছেন।</string> + + <string name="notice_end_to_end_ok">%1$s à¦à¦¨à§à¦¡-টà§-à¦à¦¨à§à¦¡ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছে।</string> + <string name="notice_end_to_end_ok_by_you">আপনি শেষ থেকে শেষ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছেন।</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s à¦à¦¨à§à¦¡-টà§-à¦à¦¨à§à¦¡ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছে (অজানা অà§à¦¯à¦¾à¦²à¦—রিদম %2$s)।</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">আপনি শেষ-থেকে-শেষ à¦à¦¨à¦•à§à¦°à¦¿à¦ªà¦¶à¦¨ চালৠকরেছেন (অজানা অà§à¦¯à¦¾à¦²à¦—রিদম %1$s )।</string> + + <string name="key_verification_request_fallback_message">%s আপনার কীটি যাচাই করার জনà§à¦¯ অনà§à¦°à§‹à¦§ করছে, তবে আপনার কà§à¦²à¦¾à¦¯à¦¼à§‡à¦¨à§à¦Ÿ ইন-চà§à¦¯à¦¾à¦Ÿ কী যাচাইকরণ সমরà§à¦¥à¦¨ করে না। কীগà§à¦²à¦¿ যাচাই করতে আপনাকে লিগà§à¦¯à¦¾à¦¸à¦¿ কী যাচাইকরণ বà§à¦¯à¦¬à¦¹à¦¾à¦° করতে হবে।</string> + + <string name="call_notification_answer">গà§à¦°à¦¹à¦£</string> + <string name="call_notification_reject">পতন</string> + <string name="call_notification_hangup">বনà§à¦§ করà§à¦¨</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-bs/strings.xml b/matrix-sdk-android/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..6a6ee46d3243da5b9328d65c84feb0d7658a0c2d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bs/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="room_displayname_invite_from">Pozovite iz %s</string> + <string name="room_displayname_room_invite">Poziv u Sobu</string> + <string name="room_displayname_two_members">%1$s i %2$s</string> + <string name="room_displayname_empty_room">Prazna soba</string> +</resources> \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..2dc2206c8c92c7f5a6844995868b9faaa766ef5d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -0,0 +1,79 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s ha enviat una imatge.</string> + + <string name="notice_room_leave">%1s ha sortit</string> + <string name="notice_room_join">%1s ha entrat</string> + <string name="medium_phone_number">Número de telèfon</string> + + <string name="medium_email">Correu electrònic</string> + <string name="encrypted_message">Missatge encriptat</string> + + <string name="notice_room_invite_no_invitee">la invitació de %s</string> + <string name="notice_room_invite">%1$s ha convidat a %2$s</string> + <string name="notice_room_invite_you">%1$s us ha convidat</string> + <string name="notice_room_reject">%1$s ha rebutjat la invitació</string> + <string name="notice_room_kick">%1$s ha fet fora a %2$s</string> + + + <string name="notice_display_name_changed_from">%1$s ha canviat el seu nom visible de %2$s a %3$s</string> + <string name="notice_display_name_removed">%1$s ha eliminat el seu nom visible (%2$s)</string> + <string name="notice_room_topic_changed">%1$s ha canviat el tema a: %2$s</string> + <string name="notice_room_name_changed">%1$s ha canviat el nom de la sala a: %2$s</string> + <string name="notice_answered_call">%s ha contestat la trucada.</string> + <string name="notice_ended_call">%s ha finalitzat la trucada.</string> + <string name="notice_room_visibility_invited">tots el membres de la sala, des del punt en què són convidats.</string> + <string name="notice_room_visibility_shared">tots els membres de la sala.</string> + <string name="notice_room_visibility_unknown">desconegut (%s).</string> + <string name="notice_end_to_end">%1$s ha activat l\'encriptació d\'extrem a extrem (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s ha sol·licitat una conferència VoIP</string> + <string name="notice_room_unban">%1$s ha readmès a %2$s</string> + <string name="notice_room_ban">%1$s ha vetat a %2$s</string> + <string name="notice_room_withdraw">%1$s ha retirat la invitació de %2$s</string> + <string name="notice_avatar_url_changed">%1$s ha canviat el seu avatar</string> + <string name="notice_made_future_room_visibility">%1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara</string> + <string name="notice_room_visibility_joined">tots els membres de la sala, des del punt en què hi entrin.</string> + <string name="notice_room_visibility_world_readable">qualsevol.</string> + <string name="notice_voip_started">S\'ha iniciat la conferència VoIP</string> + <string name="notice_voip_finished">S\'ha finalitzat la conferència de veu IP</string> + + <string name="notice_avatar_changed_too">(s\'ha canviat també l\'avatar)</string> + <string name="notice_room_name_removed">%1$s ha eliminat el nom de la sala</string> + <string name="notice_room_topic_removed">%1$s ha eliminat el tema de la sala</string> + <string name="notice_profile_change_redacted">%1$s ha actualitzat el seu perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s ha enviat una invitació a %2$s per a entrar a la sala</string> + <string name="notice_room_third_party_registered_invite">%1$s ha acceptat la invitació per a %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** No s\'ha pogut desencriptar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">El dispositiu del remitent no ens ha enviat les claus per aquest missatge.</string> + + <string name="could_not_redact">No s\'ha pogut redactar</string> + <string name="unable_to_send_message">No s\'ha pogut enviar el missatge</string> + + <string name="message_failed_to_upload">No s\'ha pogut pujar la imatge</string> + + <string name="network_error">S\'ha produït un error de xarxa</string> + <string name="matrix_error">S\'ha produït un error de Matrix</string> + + <string name="room_error_join_failed_empty_room">Actualment no es pot tornar a entrar a una sala buida.</string> + + <string name="notice_display_name_set">%1$s a canviat el seu nom visible a %2$s</string> + <string name="notice_placed_video_call">%s ha iniciat una trucada de vÃdeo.</string> + <string name="notice_placed_voice_call">%s ha iniciat una trucada de veu.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Convidat per %s</string> + <string name="room_displayname_room_invite">Convideu a la sala</string> + <string name="room_displayname_two_members">%1$s i %2$s</string> + <string name="room_displayname_empty_room">Sala buida</string> + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s i 1 altre</item> + <item quantity="other">%1$s i %2$d altres</item> + </plurals> + + + <string name="summary_user_sent_sticker">%1$s ha enviat un adhesiu.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..44908c38f7c8b998ca70c1201a2037a6edbce471 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -0,0 +1,171 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">Uživatel %1$s poslal obrázek.</string> + <string name="summary_user_sent_sticker">Uživatel %1$s poslal nálepku.</string> + + <string name="notice_room_invite_no_invitee">Pozvánka od uživatele %s</string> + <string name="notice_room_invite">Uživatel %1$s pozval uživatele %2$s</string> + <string name="notice_room_invite_you">Uživatel %1$s vás pozval</string> + <string name="notice_room_join">Uživatel %1$s se pÅ™ipojil</string> + <string name="notice_room_leave">Uživatel %1$s odeÅ¡el</string> + <string name="notice_room_reject">Uživatel %1$s odmÃtl pozvánÃ</string> + <string name="notice_room_kick">Uživatel %1$s vykopl uživatele %2$s</string> + <string name="notice_room_unban">Uživatel %1$s znovu povolil vstup uživateli %2$s</string> + <string name="notice_room_ban">Uživatel %1$s vykázal uživatele %2$s</string> + <string name="notice_room_withdraw">Uživatel %1$s zruÅ¡il pozvánà pro uživatele %2$s</string> + <string name="notice_avatar_url_changed">Uživatel %1$s zmÄ›nil svůj profilový obrázek</string> + <string name="notice_display_name_set">Uživatel %1$s nastavil své zobrazované jméno na %2$s</string> + <string name="notice_display_name_changed_from">Uživatel %1$s zmÄ›nil své zobrazované jméno z %2$s na %3$s</string> + <string name="notice_display_name_removed">Uživatel %1$s odstranil své zobrazované jméno (%2$s)</string> + <string name="notice_room_topic_changed">Uživatel %1$s zmÄ›nil téma na: %2$s</string> + <string name="notice_room_name_changed">Uživatel %1$s zmÄ›nil název mÃstnosti na: %2$s</string> + <string name="notice_placed_video_call">Uživatel %s uskuteÄnil videohovor.</string> + <string name="notice_placed_voice_call">Uživatel %s uskuteÄnil hlasový hovor.</string> + <string name="notice_answered_call">Uživatel %s pÅ™ijal hovor.</string> + <string name="notice_ended_call">Uživatel %s ukonÄil hovor.</string> + <string name="notice_made_future_room_visibility">Uživatel %1$s nastavit viditelnost budoucÃch zpráv v mÃstnosti pro %2$s</string> + <string name="notice_room_visibility_invited">vÅ¡echny Äleny mÃstnosti od chvÃle, kdy budou pozváni.</string> + <string name="notice_room_visibility_joined">vÅ¡echny Äleny mÃstnosti od chvÃle, kdy se pÅ™ipojÃ.</string> + <string name="notice_room_visibility_shared">vÅ¡echny Äleny mÃstnosti.</string> + <string name="notice_room_visibility_world_readable">kohokoliv.</string> + <string name="notice_room_visibility_unknown">neznámým (%s).</string> + <string name="notice_end_to_end">Uživatel %1$s zapnul end-to-end Å¡ifrovánà (%2$s)</string> + + <string name="notice_requested_voip_conference">Uživatel %1$s požádal o VoIP konferenci</string> + <string name="notice_voip_started">ZaÄala VoIP konference</string> + <string name="notice_voip_finished">VoIP konference skonÄila</string> + + <string name="notice_avatar_changed_too">(profilový obrázek byl také zmÄ›nÄ›n)</string> + <string name="notice_room_name_removed">Uživatel %1$s odstranil název mÃstnosti</string> + <string name="notice_room_topic_removed">Uživatel %1$s odstranil téma mÃstnosti</string> + <string name="notice_profile_change_redacted">Uživatel %1$s aktualizoval svůj profil %2$s</string> + <string name="notice_room_third_party_invite">Uživatel %1$s do této mÃstnosti pozval uživatele %2$s</string> + <string name="notice_room_third_party_registered_invite">Uživatel %1$s pÅ™ijal pozvánà pro %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Nelze deÅ¡ifrovat: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">OdesÃlatelovo zaÅ™Ãzenà neposlalo klÃÄe pro tuto zprávu.</string> + + <string name="could_not_redact">Nelze vymazat</string> + <string name="unable_to_send_message">Zprávu nelze odeslat</string> + + <string name="message_failed_to_upload">Obrázek nelze nahrát</string> + + <string name="network_error">Chyba sÃtÄ›</string> + <string name="matrix_error">Chyba v Matrixu</string> + + <string name="room_error_join_failed_empty_room">V souÄasnosti nenà možné se znovu pÅ™ipojit do prázdné mÃstnosti.</string> + + <string name="encrypted_message">Å ifrovaná zpráva</string> + + <string name="medium_email">E-mailová adresa</string> + <string name="medium_phone_number">Telefonnà ÄÃslo</string> + + <string name="room_displayname_invite_from">Pozvánà od %s</string> + <string name="room_displayname_room_invite">Pozvánà do mÃstnosti</string> + + <string name="room_displayname_two_members">%1$s a %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s a jeden dalÅ¡Ã</item> + <item quantity="few">%1$s a %2$d dalÅ¡Ã</item> + <item quantity="other">%1$s a %2$d dalÅ¡Ãch</item> + </plurals> + + <string name="room_displayname_empty_room">Prázdná mÃstnost</string> + + <string name="notice_room_update">Uživatel %s upgradoval tuto mÃstnost.</string> + + <string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Zpráva smazána uživatelem %1$s [důvod: %2$s]</string> + <string name="notice_room_third_party_revoked_invite">Uživatel %1$s obnovil pozvánku do mÃstnosti pro uživatele %2$s</string> + <string name="verification_emoji_cat">KoÄka</string> + <string name="verification_emoji_lion">Lev</string> + <string name="verification_emoji_horse">Kůň</string> + <string name="verification_emoji_unicorn">Jednorožec</string> + <string name="verification_emoji_pig">Prase</string> + <string name="verification_emoji_elephant">Slon</string> + <string name="verification_emoji_rabbit">KrálÃk</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kohout</string> + <string name="verification_emoji_penguin">TuÄňák</string> + <string name="verification_emoji_turtle">Želva</string> + <string name="verification_emoji_fish">Ryba</string> + <string name="verification_emoji_octopus">Chobotnice</string> + <string name="verification_emoji_butterfly">Motýl</string> + <string name="verification_emoji_flower">KvÄ›tina</string> + <string name="verification_emoji_tree">Strom</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Houba</string> + <string name="verification_emoji_globe">ZemÄ›koule</string> + <string name="verification_emoji_moon">MÄ›sÃc</string> + <string name="verification_emoji_cloud">Mrak</string> + <string name="verification_emoji_fire">Oheň</string> + <string name="verification_emoji_banana">Banán</string> + <string name="verification_emoji_apple">Jablko</string> + <string name="verification_emoji_strawberry">Jahoda</string> + <string name="verification_emoji_corn">KukuÅ™ice</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Dort</string> + <string name="verification_emoji_heart">Srdce</string> + <string name="verification_emoji_smiley">SmajlÃk</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Klobouk</string> + <string name="verification_emoji_glasses">Brýle</string> + <string name="verification_emoji_santa">Santa Klaus</string> + <string name="verification_emoji_thumbsup">Zvednutý palec</string> + <string name="verification_emoji_umbrella">DeÅ¡tnÃk</string> + <string name="verification_emoji_hourglass">PÅ™esÃpacà hodiny</string> + <string name="verification_emoji_clock">Hodiny</string> + <string name="verification_emoji_gift">Dárek</string> + <string name="verification_emoji_lightbulb">Žárovka</string> + <string name="verification_emoji_book">Kniha</string> + <string name="verification_emoji_pencil">Tužka</string> + <string name="verification_emoji_paperclip">Sponka</string> + <string name="verification_emoji_scissors">Nůžky</string> + <string name="verification_emoji_lock">Zámek</string> + <string name="verification_emoji_key">KlÃÄ</string> + <string name="verification_emoji_hammer">Kladivo</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Vlajka</string> + <string name="verification_emoji_train">Vlak</string> + <string name="verification_emoji_bicycle">JÃzdnà kolo</string> + <string name="verification_emoji_airplane">Letadlo</string> + <string name="verification_emoji_rocket">Raketa</string> + <string name="verification_emoji_trophy">Trofej</string> + <string name="verification_emoji_ball">MÃÄ</string> + <string name="verification_emoji_guitar">Kytara</string> + <string name="verification_emoji_trumpet">Trumpeta</string> + <string name="verification_emoji_bell">Zvon</string> + <string name="verification_emoji_anchor">Kotva</string> + <string name="verification_emoji_headphone">Sluchátka</string> + <string name="verification_emoji_folder">Desky</string> + <string name="initial_sync_start_importing_account">Úvodnà synchronizace: +\nImport úÄtu…</string> + <string name="initial_sync_start_importing_account_crypto">Úvodnà synchronizace: +\nImport klÃÄů</string> + <string name="initial_sync_start_importing_account_rooms">Úvodnà synchronizace: +\nImport mÃstnostÃ</string> + <string name="initial_sync_start_importing_account_joined_rooms">Úvodnà synchronizace: +\nImport mÃstnostÃ, kterými jste Äleny</string> + <string name="initial_sync_start_importing_account_left_rooms">Úvodnà synchronizace: +\nImport opuÅ¡tÄ›ných mÃstnostÃ</string> + <string name="initial_sync_start_importing_account_groups">Úvodnà synchronizace: +\nImport skupin</string> + <string name="initial_sync_start_importing_account_data">Úvodnà synchronizace: +\nImport dat úÄtu</string> + + <string name="event_status_sending_message">OdesÃlánà zprávy…</string> + <string name="verification_emoji_wrench">Maticový klÃÄ</string> + <string name="verification_emoji_pin">PÅ™ipÃnáÄek</string> + + <string name="initial_sync_start_importing_account_invited_rooms">Úvodnà synchronizace: +\nImport pozvánek</string> + <string name="clear_timeline_send_queue">Vymazat frontu neodeslaných zpráv</string> + + <string name="notice_room_invite_with_reason">Uživatel %1$s pozval uživatele %2$s. Důvod: %3$s</string> + <string name="notice_room_invite_you_with_reason">Uživatel %1$s váš pozval. Důvod: %2$s</string> + <string name="notice_room_leave_with_reason">Uživatel %1$s odeÅ¡el. Důvod: %2$s</string> + <string name="notice_event_redacted">Zpráva odstranÄ›na</string> + <string name="notice_event_redacted_by">Zprávu odstranil/a %1$s</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-da/strings.xml b/matrix-sdk-android/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..510fa231afe3660cd953d0a4ed85f98a242480bb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-da/strings.xml @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s sendte et billede.</string> + + <string name="notice_room_invite_no_invitee">%ss invitation</string> + <string name="notice_room_invite">%1$s inviterede %2$s</string> + <string name="notice_room_invite_you">%1$s inviterede dig</string> + <string name="notice_room_join">%1$s forbandt</string> + <string name="notice_room_leave">%1$s forlod rummet</string> + <string name="notice_room_reject">%1$s afviste invitationen</string> + <string name="notice_room_kick">%1$s kickede %2$s</string> + <string name="notice_room_unban">%1$s unbannede %2$s</string> + <string name="notice_room_ban">%1$s bannede %2$s</string> + <string name="notice_room_withdraw">%1$s trak %2$ss invitation tilbage</string> + <string name="notice_avatar_url_changed">%1$s skiftede sin avatar</string> + <string name="notice_display_name_set">%1$s satte sit viste navn til %2$s</string> + <string name="notice_display_name_changed_from">%1$s ændrede sit viste navn fra %2$s til %3$s</string> + <string name="notice_display_name_removed">%1$s fjernede sit viste navn (%2$s)</string> + <string name="notice_room_topic_changed">%1$s ændrede emnet til: %2$s</string> + <string name="notice_room_name_changed">%1$s ændrede rumnavnet til: %2$s</string> + <string name="notice_placed_video_call">%s startede et videoopkald.</string> + <string name="notice_placed_voice_call">%s startede et stemmeopkald.</string> + <string name="notice_answered_call">%s svarede opkaldet.</string> + <string name="notice_ended_call">%s stoppede opkaldet.</string> + <string name="notice_made_future_room_visibility">%1$s gjorde den fremtidige rum historik synlig for %2$s</string> + <string name="notice_room_visibility_invited">alle medlemmer af rummet, fra det tidspunkt de er inviteret.</string> + <string name="notice_room_visibility_joined">alle medlemmer af rummet, fra det tidspunkt de er forbundede.</string> + <string name="notice_room_visibility_shared">Alle medlemmer af rummet.</string> + <string name="notice_room_visibility_world_readable">alle.</string> + <string name="notice_room_visibility_unknown">ukendt (%s).</string> + <string name="notice_end_to_end">%1$s slog ende-til-ende kryptering til (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s forespurgte en VoIP konference</string> + <string name="notice_voip_started">VoIP konference startet</string> + <string name="notice_voip_finished">VoIP konference afsluttet</string> + + <string name="notice_avatar_changed_too">(avatar blev ogsÃ¥ ændret)</string> + <string name="notice_room_name_removed">%1$s fjernede navnet pÃ¥ rummet</string> + <string name="notice_room_topic_removed">%1$s fjernede emnet for rummet</string> + <string name="notice_profile_change_redacted">%1$s opdaterede sin profil %2$s</string> + <string name="notice_room_third_party_invite">%1$s inviterede %2$s til rummet</string> + <string name="notice_room_third_party_registered_invite">%1$s accepterede invitationen til %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Kunne ikke dekryptere: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Afsenderens enhed har ikke sendt os nøglerne til denne besked.</string> + + <string name="could_not_redact">Kunne ikke hemmeligholde</string> + <string name="unable_to_send_message">Kunne ikke sende besked</string> + + <string name="message_failed_to_upload">Kunne ikke uploade billede</string> + + <string name="network_error">Netværks fejl</string> + <string name="matrix_error">Matrix fejl</string> + + <string name="room_error_join_failed_empty_room">Det er i øjeblikket ikke muligt at genforbinde til et tomt rum.</string> + + <string name="encrypted_message">Krypteret besked</string> + + <string name="medium_email">mailadresse</string> + <string name="medium_phone_number">Telefonnummer</string> + + <string name="room_displayname_invite_from">Invitation fra %s</string> + <string name="room_displayname_room_invite">Invitation til rum</string> + <string name="room_displayname_two_members">%1$s og %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s og 1 anden</item> + <item quantity="other">%1$s og %2$d andre</item> + </plurals> + + <string name="room_displayname_empty_room">Tomt rum</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ec9240067cddb96a69e4c66400c797ca6f513c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -0,0 +1,306 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s hat ein Bild gesendet.</string> + + <string name="notice_room_invite_no_invitee">Einladung von %s</string> + <string name="notice_room_invite">%1$s hat %2$s eingeladen</string> + <string name="notice_room_invite_you">%1$s hat dich eingeladen</string> + <string name="notice_room_join">%1$s hat den Raum betreten</string> + <string name="notice_room_leave">%1$s hat den Raum verlassen</string> + <string name="notice_room_reject">%1$s hat die Einladung abgelehnt</string> + <string name="notice_room_kick">%1$s hat %2$s gekickt</string> + <string name="notice_room_unban">%1$s hat die Sperre von %2$s aufgehoben</string> + <string name="notice_room_ban">%1$s hat %2$s verbannt</string> + <string name="notice_room_withdraw">%1$s hat die Einladung für %2$s zurückgezogen</string> + <string name="notice_avatar_url_changed">%1$s hat das Profilbild geändert</string> + <string name="notice_display_name_set">%1$s hat den Anzeigenamen geändert in %2$s</string> + <string name="notice_display_name_changed_from">%1$s hat den Anzeigenamen von %2$s auf %3$s geändert</string> + <string name="notice_display_name_removed">%1$s hat den Anzeigenamen gelöscht (%2$s)</string> + <string name="notice_room_topic_changed">%1$s hat das Raumthema geändert auf: %2$s</string> + <string name="notice_room_name_changed">%1$s hat den Raumnamen geändert in: %2$s</string> + <string name="notice_placed_video_call">%s hat einen Videoanruf durchgeführt.</string> + <string name="notice_placed_voice_call">%s hat einen Sprachanruf getätigt.</string> + <string name="notice_answered_call">%s hat den Anruf angenommen.</string> + <string name="notice_ended_call">%s hat den Anruf beendet.</string> + <string name="notice_made_future_room_visibility">%1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s</string> + <string name="notice_room_visibility_invited">Alle Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden).</string> + <string name="notice_room_visibility_joined">Alle Mitglieder (ab dem Zeitpunkt, an dem sie den Raum betreten haben).</string> + <string name="notice_room_visibility_shared">alle Raum-Mitglieder.</string> + <string name="notice_room_visibility_world_readable">Jeder.</string> + <string name="notice_room_visibility_unknown">Unbekannt (%s).</string> + <string name="notice_end_to_end">%1$s hat die Ende-zu-Ende-Verschlüsselung aktiviert (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s möchte eine VoIP-Konferenz beginnen</string> + <string name="notice_voip_started">VoIP-Konferenz gestartet</string> + <string name="notice_voip_finished">VoIP-Konferenz beendet</string> + + <string name="notice_avatar_changed_too">(Profilbild wurde ebenfalls geändert)</string> + <string name="notice_room_name_removed">%1$s hat den Raumnamen entfernt</string> + <string name="notice_room_topic_removed">%1$s hat das Raum-Thema entfernt</string> + <string name="notice_profile_change_redacted">%1$s hat das Benutzerprofil aktualisiert %2$s</string> + <string name="notice_room_third_party_invite">%1$s hat eine Einladung an %2$s gesendet</string> + <string name="notice_room_third_party_registered_invite">%1$s hat die Einladung in %2$s akzeptiert</string> + + <string name="notice_crypto_unable_to_decrypt">** Nicht entschlüsselbar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Das absendende Gerät hat uns keine Schlüssel für diese Nachricht übermittelt.</string> + + <!-- Room Screen --> + <string name="could_not_redact">Entfernen nicht möglich</string> + <string name="unable_to_send_message">Nachricht kann nicht gesendet werden</string> + + <string name="message_failed_to_upload">Bild konnte nicht hochgeladen werden</string> + + <!-- general errors --> + <string name="network_error">Netzwerk-Fehler</string> + <string name="matrix_error">Matrix-Fehler</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Es ist aktuell nicht möglich, einen leeren Raum erneut zu betreten.</string> + + <string name="encrypted_message">Verschlüsselte Nachricht</string> + + <!-- medium friendly name --> + <string name="medium_email">E-Mail-Adresse</string> + <string name="medium_phone_number">Telefonnummer</string> + + <string name="summary_user_sent_sticker">%1$s sandte einen Sticker.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Einladung von %s</string> + <string name="room_displayname_room_invite">Raumeinladung</string> + <string name="room_displayname_two_members">%1$s und %2$s</string> + <string name="room_displayname_empty_room">Leerer Raum</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s und 1 andere(r)</item> + <item quantity="other">%1$s und %2$d andere</item> + </plurals> + + + <string name="notice_event_redacted">Nachricht entfernt</string> + <string name="notice_event_redacted_by">Nachricht entfernt von %1$s</string> + <string name="notice_event_redacted_with_reason">Nachricht entfernt [Grund: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Nachricht entfernt von %1$s [Grund: %2$s]</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_dog">Hund</string> + <string name="verification_emoji_cat">Katze</string> + <string name="verification_emoji_lion">Löwe</string> + <string name="verification_emoji_horse">Pferd</string> + <string name="verification_emoji_unicorn">Einhorn</string> + <string name="verification_emoji_pig">Schwein</string> + <string name="verification_emoji_elephant">Elefant</string> + <string name="verification_emoji_rabbit">Kaninchen</string> + <string name="notice_room_update">%s hat diesen Raum aufgewertet.</string> + + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Hahn</string> + <string name="verification_emoji_penguin">Pinguin</string> + <string name="verification_emoji_turtle">Schildkröte</string> + <string name="verification_emoji_fish">Fisch</string> + <string name="verification_emoji_octopus">Oktopus</string> + <string name="verification_emoji_butterfly">Schmetterling</string> + <string name="verification_emoji_flower">Blume</string> + <string name="verification_emoji_tree">Baum</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Pilz</string> + <string name="verification_emoji_globe">Globus</string> + <string name="verification_emoji_moon">Mond</string> + <string name="verification_emoji_cloud">Wolke</string> + <string name="verification_emoji_fire">Feuer</string> + <string name="verification_emoji_banana">Banane</string> + <string name="verification_emoji_apple">Apfel</string> + <string name="verification_emoji_strawberry">Erdbeere</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_cake">Kuchen</string> + <string name="verification_emoji_heart">Herz</string> + <string name="verification_emoji_smiley">Smiley</string> + <string name="verification_emoji_robot">Roboter</string> + <string name="verification_emoji_hat">Hut</string> + <string name="verification_emoji_glasses">Brille</string> + <string name="verification_emoji_wrench">Schraubenschlüssel</string> + <string name="verification_emoji_santa">Weihnachtsmann</string> + <string name="verification_emoji_thumbsup">Daumen hoch</string> + <string name="verification_emoji_umbrella">Regenschirm</string> + <string name="verification_emoji_hourglass">Sanduhr</string> + <string name="verification_emoji_clock">Uhr</string> + <string name="verification_emoji_gift">Geschenk</string> + <string name="verification_emoji_lightbulb">Glühbirne</string> + <string name="verification_emoji_book">Buch</string> + <string name="verification_emoji_pencil">Bleistift</string> + <string name="verification_emoji_paperclip">Büroklammer</string> + <string name="verification_emoji_scissors">Schere</string> + <string name="verification_emoji_lock">Schloss</string> + <string name="verification_emoji_key">Schlüssel</string> + <string name="verification_emoji_hammer">Hammer</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Flagge</string> + <string name="verification_emoji_train">Zug</string> + <string name="verification_emoji_bicycle">Fahrrad</string> + <string name="verification_emoji_airplane">Flugzeug</string> + <string name="verification_emoji_rocket">Rakete</string> + <string name="verification_emoji_trophy">Pokal</string> + <string name="verification_emoji_ball">Ball</string> + <string name="verification_emoji_guitar">Gitarre</string> + <string name="verification_emoji_trumpet">Trompete</string> + <string name="verification_emoji_bell">Glocke</string> + <string name="verification_emoji_anchor">Anker</string> + <string name="verification_emoji_headphone">Kopfhörer</string> + <string name="verification_emoji_folder">Ordner</string> + <string name="verification_emoji_pin">Stecknadel</string> + + <string name="event_status_sending_message">Sende eine Nachricht…</string> + <string name="clear_timeline_send_queue">Sendewarteschlange leeren</string> + + <string name="initial_sync_start_importing_account">Erste Synchronisation: Importiere Benutzerkonto…</string> + <string name="initial_sync_start_importing_account_crypto">Erste Synchronisation: Importiere Cryptoschlüssel</string> + <string name="initial_sync_start_importing_account_rooms">Erste Synchronisation: Importiere Räume</string> + <string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation: Importiere betretene Räume</string> + <string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation: Importiere eingeladene Räume</string> + <string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation: Importiere verlassene Räume</string> + <string name="initial_sync_start_importing_account_groups">Erste Synchronisation: Importiere Gemeinschaften</string> + <string name="initial_sync_start_importing_account_data">Erste Synchronisation: Importiere Benutzerdaten</string> + + <string name="notice_room_third_party_revoked_invite">%1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen</string> + <string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s hat dich eingeladen. Grund: %2$s</string> + <string name="notice_room_join_with_reason">%1$s ist dem Raum beigetreten. Grund: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s hat den Raum verlassen. Grund: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s hat die Einladung abgelehnt. Grund: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s hat %2$s gekickt. Grund: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s hat Sperre von %2$s aufgehoben. Grund: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s hat %2$s verbannt. Grund: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s hat eine Einladung an %2$s gesandt um diesem Raum beizutreten. Grund: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s hat Einladung an %2$s zu Betreten dieses Raumes zurückgezogen. Grund: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s hat die Einladung für %2$s angenommen. Grund: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s hat Einladung für %2$s verworfen. Grund: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s fügt %2$s als eine Adresse für diesen Raum hinzu.</item> + <item quantity="other">%1$s fügt %2$s als Adressen für diesen Raum hinzu.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s entfernt %2$s als eine Adresse für diesen Raum.</item> + <item quantity="other">%1$s entfernt %2$s als Adressen für diesen Raum.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s fügt %2$s als Adresse für diesen Raum hinzu und entfernt %3$s.</string> + + <string name="notice_room_canonical_alias_set">%1$s legt die Hauptadresse fest für diesen Raum als %2$s fest.</string> + <string name="notice_room_canonical_alias_unset">%1$s entfernt die Hauptadresse für diesen Raum.</string> + + <string name="notice_room_guest_access_can_join">%1$s hat Gästen erlaubt den Raum zu betreten.</string> + <string name="notice_room_guest_access_forbidden">%1$s hat Gäste unterbunden den Raum zu betreten.</string> + + <string name="notice_end_to_end_ok">%1$s aktivierte Ende-zu-Ende-Verschlüsselung.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s).</string> + + <string name="key_verification_request_fallback_message">%s fordert zur Ãœberprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen.</string> + + <string name="summary_you_sent_image">Du hast ein Bild gesendet.</string> + <string name="summary_you_sent_sticker">Du hast einen Sticker gesendet.</string> + + <string name="notice_room_invite_no_invitee_by_you">Deine Einladung</string> + <string name="notice_room_created">%1$s hat den Raum erstellt</string> + <string name="notice_room_created_by_you">Du hast den Raum erstellt</string> + <string name="notice_room_invite_by_you">Du hast $1$s eingeladen</string> + <string name="notice_room_join_by_you">Du bist dem Raum beigetreten</string> + <string name="notice_room_leave_by_you">Du hast den Raum verlassen</string> + <string name="notice_room_reject_by_you">Du hast die Einladung abgelehnt</string> + <string name="notice_room_kick_by_you">Du hast %1$s aus dem Raum entfernt</string> + <string name="notice_room_unban_by_you">Du hast den Bann von %1$s aufgehoben</string> + <string name="notice_room_ban_by_you">Du hast %1$s gebannt</string> + <string name="notice_room_withdraw_by_you">Du hast die Einladung von %1$s zurückgenommen</string> + <string name="notice_avatar_url_changed_by_you">Du hast dein Profilbild geändert</string> + <string name="notice_display_name_set_by_you">Du hast deinen Anzeigenamen zu %1$s geändert</string> + <string name="notice_display_name_changed_from_by_you">Du hast deinen Anzeigenamen von %1$s zu %2$s geändert</string> + <string name="notice_display_name_removed_by_you">Du hast deinen Anzeigenamen entfernt (er war %1$s)</string> + <string name="notice_room_topic_changed_by_you">Du hast das Thema geändert auf: %1$s</string> + <string name="notice_room_avatar_changed">%1$s hat das Bild des Raumes geändert</string> + <string name="notice_room_avatar_changed_by_you">Du hast das Bild des Raumes geändert</string> + <string name="notice_room_name_changed_by_you">Du hast den Raumnamen zu %1$s geändert</string> + <string name="notice_placed_video_call_by_you">Du hast einen Videoanruf gestartet.</string> + <string name="notice_placed_voice_call_by_you">Du hast einen Audioanruf gestartet.</string> + <string name="notice_answered_call_by_you">Du hast den Anruf angenommen.</string> + <string name="notice_ended_call_by_you">Du hast den Anruf beendet.</string> + <string name="notice_made_future_room_visibility_by_you">Du hast den zukünftigen Nachrichtenverlauf für %1$s sichtbar gemacht</string> + <string name="notice_end_to_end_by_you">Du hast Ende-zu-Ende-Verschlüsselung aktiviert (%1$s)</string> + <string name="notice_room_update_by_you">Du hast den Raum aufgwertet.</string> + + <string name="notice_requested_voip_conference_by_you">Du hast eine VoIP-Konferenz angefordert</string> + <string name="notice_room_name_removed_by_you">Du hast den Raumnamen entfernt</string> + <string name="notice_room_topic_removed_by_you">Du hast das Raumthema entfernt</string> + <string name="notice_room_avatar_removed">%1$s hat das Bild des Raumes entfernt</string> + <string name="notice_room_avatar_removed_by_you">Du hast das Bild des Raumes entfernt</string> + <string name="notice_profile_change_redacted_by_you">Du hast dein Profil %1$s aktualisiert</string> + <string name="notice_room_third_party_invite_by_you">Du hast %1$s in den Raum eingeladen</string> + <string name="notice_room_third_party_revoked_invite_by_you">Du hast die Einladung für %1$s zurückgenommen</string> + <string name="notice_room_third_party_registered_invite_by_you">Du hast die Einladung für %1$s akzeptiert</string> + + <string name="notice_widget_added">%1$s hat das %2$s Widget hinzugefügt</string> + <string name="notice_widget_added_by_you">Du hast das %1$s Widget hinzugefügt</string> + <string name="notice_widget_removed">%1$s hat das %2$s Widget entfernt</string> + <string name="notice_widget_removed_by_you">Du hast das %1$s Widget entfernt</string> + <string name="notice_widget_modified">%1$s hat das %2$s Widget modifiziert</string> + <string name="notice_widget_modified_by_you">Du hast das %1$s Widget modifiziert</string> + + <string name="power_level_admin">Administrator</string> + <string name="power_level_moderator">Moderator</string> + <string name="power_level_default">Standard</string> + <string name="power_level_custom">Benutzerdefiniert (%1$d)</string> + <string name="power_level_custom_no_value">Benutzerdefiniert</string> + + <string name="notice_power_level_changed_by_you">Du hast die Berechtigungsstufe von %1$s geändert.</string> + <string name="notice_power_level_changed">%1$s hat die Berechtigungsstufe von %2$s geändert.</string> + <string name="notice_power_level_diff">%1$s von %2$s zu %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">Deine Einladung. Grund: %1$s</string> + <string name="notice_room_invite_with_reason_by_you">Du hast %1$s eingeladen. Grund: %2$s</string> + <string name="notice_room_join_with_reason_by_you">Du bist dem Raum beigetreten. Grund: %1$s</string> + <string name="notice_room_leave_with_reason_by_you">Du hast den Raum verlassen. Grund: %1$s</string> + <string name="notice_room_reject_with_reason_by_you">Du hast die Einladung abgelehnt. Grund: %1$s</string> + <string name="notice_room_kick_with_reason_by_you">Du hast %1$s aus dem Raum entfernt. Grund %2$s</string> + <string name="notice_room_unban_with_reason_by_you">Du hast den Bann von %1$s aufgehoben. Grund: %2$s</string> + <string name="notice_room_ban_with_reason_by_you">Du hast %1$s gebannt. Grund: %2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Du hast %1$s in den Raum eingeladen. Grund: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Du hast die Einladung für %1$s zurückgenommen. Grund: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Du hast die Einladung von %1$s angenommen. Grund: %2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">Du hast die Einladung von %1$s abgelehnt. Grund: %2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">Du hast die Raumaddresse %1$s hinzugefügt.</item> + <item quantity="other">Du hast die Raumaddressen %1$s hinzugefügt.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">Du hast die Raumaddresse %1$s vom Raum entfernt.</item> + <item quantity="other">Du hast die Raumaddressen %1$s vom Raum entfernt.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">Du hast den Raumaddressen %1$s hinzugefügt und %2$s entfernt.</string> + + <string name="notice_room_canonical_alias_set_by_you">Du hast die Hauptaddresse für diesen Raum auf %1$s gesetzt.</string> + <string name="notice_room_canonical_alias_unset_by_you">Du hast die Hauptaddresse des Raums entfernt.</string> + + <string name="notice_room_guest_access_can_join_by_you">Du hast Gästen erlaubt dem Raum beizutreten.</string> + <string name="notice_room_guest_access_forbidden_by_you">Du hast Gästen untersagt dem Raum beizutreten.</string> + + <string name="notice_end_to_end_ok_by_you">Du hast Ende-zu-Ende-Verschlüsselung aktiviert.</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Du hast Ende-zu-Ende-Verschlüsselung aktiviert (unbekannter Algorithmus %1$s).</string> + + <string name="call_notification_answer">Akzeptiere</string> + <string name="call_notification_reject">Ablehnen</string> + <string name="call_notification_hangup">Anruf beenden</string> + + <string name="notice_call_candidates">%s hat Daten gesendet, um einen Anruf zu starten.</string> + <string name="notice_call_candidates_by_you">Du hast Daten geschickt, um eine Anruf zu starten.</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-el/strings.xml b/matrix-sdk-android/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..9db4e918496520ddd054472b4d881b178101781e --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-el/strings.xml @@ -0,0 +1,76 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="medium_email">ΗλεκτÏονική διεÏθυνση</string> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">Ο/Η %1$s Îστειλε μια εικόνα.</string> + <string name="summary_user_sent_sticker">Ο/Η %1$s Îστειλε Îνα αυτοκόλλητο.</string> + + <string name="notice_room_invite_you">Ο/Η %1$s σας Ï€Ïοσκάλεσε</string> + <string name="notice_room_leave">Ο/Η %1$s αποχώÏησε</string> + <string name="notice_room_reject">Ο/Η %1$s απÎÏÏιψε την Ï€Ïόσκληση</string> + <string name="notice_room_kick">Ο/Η %1$s Îδιωξε τον/την %2$s</string> + <string name="notice_room_invite">Ο/Η %1$s Ï€Ïοσκάλεσε τον/την %2$s</string> + <string name="notice_room_invite_no_invitee">Η Ï€Ïόσκληση του/της %s</string> + <string name="medium_phone_number">ΑÏιθμός τηλεφώνου</string> + + <string name="notice_room_ban">Ο/Η %1$s απÎκλεισε τον/την %2$s</string> + <string name="notice_room_withdraw">Ο/Η %1$s απÎσυÏε την Ï€Ïόσκληση του/της %2$s</string> + <string name="notice_avatar_url_changed">Ο/Η %1$s άλλαξε εικονίδιο χÏήστη</string> + <string name="notice_display_name_set">Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα σε %2$s</string> + <string name="notice_display_name_changed_from">Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα από %2$s σε %3$s</string> + <string name="notice_display_name_removed">Ο/Η %1$s αφαίÏεσε το εμφανιζόμενό του/της όνομα (%2$s)</string> + <string name="notice_room_topic_changed">Ο/Η %1$s άλλαξε το θÎμα σε: %2$s</string> + <string name="notice_room_name_changed">Ο/Η %1$s άλλαξε το όνομα του δωματίου σε: %2$s</string> + <string name="notice_answered_call">Ο/Η %s απάντησε στην κλήση.</string> + <string name="notice_ended_call">Ο/Η %s τεÏμάτισε την κλήση.</string> + + <string name="notice_placed_video_call">Ο/Η %s Ï€Ïαγματοποίησε μια κλήση βίντεο.</string> + <string name="notice_placed_voice_call">Ο/Η %s Ï€Ïαγματοποίησε μια κλήση ήχου.</string> + <string name="notice_made_future_room_visibility">Ο/Η %1$s κατÎστησε το μελλοντικό ιστοÏικό του δωματίου οÏατό στον/στην %2$s</string> + <string name="notice_room_visibility_invited">όλα τα μÎλη του δωματίου, από την στιγμή που Ï€Ïοσκλήθηκαν.</string> + <string name="notice_room_visibility_shared">όλα τα μÎλη του δωματίου.</string> + <string name="notice_room_visibility_world_readable">οποιοσδήποτε.</string> + <string name="notice_room_visibility_unknown">άγνωστος/η (%s).</string> + <string name="notice_avatar_changed_too">(Îγινε αλλαγή και του εικονιδίου χÏήστη)</string> + <string name="notice_room_name_removed">Ο/Η %1$s αφαίÏεσε το όνομα του δωματίου</string> + <string name="notice_room_topic_removed">Ο/Η %1$s αφαίÏεσε το θÎμα του δωματίου</string> + <string name="notice_profile_change_redacted">Ο/Η %1$s ανανÎωσε το Ï€Ïοφίλ του/της %2$s</string> + <string name="notice_room_third_party_registered_invite">Ο/Η %1$s δÎχτηκε την Ï€Ïόσκληση για το %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Αδυναμία αποκÏυπτογÏάφησης: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Η συσκευή του/της αποστολÎα δεν μας Îχει στείλει τα κλειδιά για αυτό το μήνυμα.</string> + + <string name="unable_to_send_message">Αποτυχία αποστολής μηνÏματος</string> + + <string name="message_failed_to_upload">Αποτυχία αναφόÏτωσης εικόνας</string> + + <string name="network_error">Σφάλμα δικτÏου</string> + <string name="matrix_error">Σφάλμα του Matrix</string> + + <string name="encrypted_message">ΚÏυπτογÏαφημÎνο μήνυμα</string> + + <string name="notice_requested_voip_conference">Ο/Η %1$s ζήτησε μια VoIP διάσκεψη</string> + <string name="notice_voip_started">Η VoIP διάσκεψη ξεκίνησε</string> + <string name="notice_voip_finished">Η VoIP διάσκεψη Îληξε</string> + + <string name="notice_room_join">Ο/Η %1$s εισήλθε στο δωμάτιο</string> + + <string name="room_displayname_invite_from">Î Ïόσκληση από %s</string> + <string name="room_displayname_room_invite">Î Ïόσκληση στο δωμάτιο</string> + + <string name="room_displayname_two_members">%1$s και %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s και 1 ακόμα</item> + <item quantity="other">%1$s και %2$d ακόμα</item> + </plurals> + + <string name="room_displayname_empty_room">Άδειο δωμάτιο</string> + + <string name="notice_room_visibility_joined">όλα τα μÎλη του δωματίου από την στιγμή που εισήλθαν.</string> + <string name="notice_end_to_end">Ο/Η %1$s ενεÏγοποίησε την κÏυπτογÏάφηση απ\'άκÏη σ\'άκÏη (%2$s)</string> + + <string name="notice_room_third_party_invite">Ο/Η %1$s Îστειλε μία Ï€Ïόσκληση στον/στην %2$s για να εισÎλθει στο δωμάτιο</string> + <string name="room_error_join_failed_empty_room">Δεν είναι δυνατή ακόμα η επανείσοδος σε Îνα άδειο δωμάτιο.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f457e30ed0236eb131814eb4bdb927b282cf9e49 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="verification_emoji_wrench">Spanner</string> + <string name="verification_emoji_airplane">Aeroplane</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..4a1e2c4c658d15b5f99d31d2f82cb887e432310f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -0,0 +1,205 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_user_sent_image">%1$s sendis bildon.</string> + <string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string> + + <string name="notice_room_invite_no_invitee">Invito de %s</string> + <string name="notice_room_invite">%1$s invitis uzanton %2$s</string> + <string name="notice_room_invite_you">%1$s invitis vin</string> + <string name="notice_room_join">%1$s alvenis</string> + <string name="notice_room_leave">%1$s foriris</string> + <string name="notice_room_reject">%1$s malakceptis la inviton</string> + <string name="notice_room_kick">%1$s forpelis uzanton %2$s</string> + <string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string> + <string name="notice_room_ban">%1$s forbaris uzanton %2$s</string> + <string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string> + <string name="notice_avatar_url_changed">%1$s ÅanÄis sian profilbildon</string> + <string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la Ålosilojn por tiu mesaÄo.</string> + + <string name="summary_message">%1$s: %2$s</string> + <string name="notice_display_name_set">%1$s ÅanÄis sian vidigan nomon al %2$s</string> + <string name="notice_display_name_changed_from">%1$s ÅanÄis sian vidigan nomon de %2$s al %3$s</string> + <string name="notice_display_name_removed">%1$s forigis sian vidigan nomon (%2$s)</string> + <string name="notice_room_topic_changed">%1$s ÅanÄis la temon al: %2$s</string> + <string name="notice_room_name_changed">%1$s ÅanÄis nomon de la ĉambro al: %2$s</string> + <string name="notice_placed_video_call">%s vidvokis.</string> + <string name="notice_placed_voice_call">%s voĉvokis.</string> + <string name="notice_answered_call">%s respondis la vokon.</string> + <string name="notice_ended_call">%s finis la vokon.</string> + <string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string> + <string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string> + <string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliÄoj.</string> + <string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string> + <string name="notice_room_visibility_world_readable">ĉiu ajn.</string> + <string name="notice_room_visibility_unknown">nekonata (%s).</string> + <string name="notice_end_to_end">%1$s Åaltis tutvojan ĉifradon (%2$s)</string> + <string name="notice_room_update">%s gradaltigis la ĉambron.</string> + + <string name="notice_event_redacted">MesaÄo foriÄis</string> + <string name="notice_event_redacted_by">MesaÄo foriÄis de %1$s</string> + <string name="notice_event_redacted_with_reason">MesaÄo foriÄis [kialo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">MesaÄo foriÄis de %1$s [kialo: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s Äisdatigis sian profilon %2$s</string> + <string name="notice_room_third_party_invite">%1$s sendis aliÄan inviton al %2$s</string> + <string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliÄan inviton por %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string> + + <string name="could_not_redact">Ne povis redakti</string> + <string name="unable_to_send_message">Ne povas sendi mesaÄon</string> + + <string name="message_failed_to_upload">Malsukcesis alÅuti bildon</string> + + <string name="network_error">Reta eraro</string> + <string name="matrix_error">Matrix-eraro</string> + + <string name="room_error_join_failed_empty_room">Nun ne eblas re-aliÄi al malplena ĉambro</string> + + <string name="encrypted_message">Ĉifrita mesaÄo</string> + + <string name="medium_email">RetpoÅtadreso</string> + <string name="medium_phone_number">Telefonnumero</string> + + <string name="room_displayname_invite_from">Invito de %s</string> + <string name="room_displayname_room_invite">Ĉambra invito</string> + + <string name="room_displayname_two_members">%1$s kaj %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s kaj 1 alia</item> + <item quantity="other">%1$s kaj %2$d aliaj</item> + </plurals> + + <string name="room_displayname_empty_room">Malplena ĉambro</string> + + + <string name="verification_emoji_dog">Hundo</string> + <string name="verification_emoji_cat">Kato</string> + <string name="verification_emoji_lion">Leono</string> + <string name="verification_emoji_horse">Ĉevalo</string> + <string name="verification_emoji_unicorn">Unukorno</string> + <string name="verification_emoji_pig">Porko</string> + <string name="verification_emoji_elephant">Elefanto</string> + <string name="verification_emoji_rabbit">Kuniklo</string> + <string name="verification_emoji_panda">Pando</string> + <string name="verification_emoji_rooster">Koko</string> + <string name="verification_emoji_penguin">Pingveno</string> + <string name="verification_emoji_turtle">Testudo</string> + <string name="verification_emoji_fish">FiÅo</string> + <string name="verification_emoji_octopus">Polpo</string> + <string name="verification_emoji_butterfly">Papilio</string> + <string name="verification_emoji_flower">Floro</string> + <string name="verification_emoji_tree">Arbo</string> + <string name="verification_emoji_cactus">Kakto</string> + <string name="verification_emoji_mushroom">Fungo</string> + <string name="verification_emoji_globe">Globo</string> + <string name="verification_emoji_moon">Luno</string> + <string name="verification_emoji_cloud">Nubo</string> + <string name="verification_emoji_fire">Fajro</string> + <string name="verification_emoji_banana">Banano</string> + <string name="verification_emoji_apple">Pomo</string> + <string name="verification_emoji_strawberry">Frago</string> + <string name="verification_emoji_corn">Maizo</string> + <string name="verification_emoji_pizza">Pico</string> + <string name="verification_emoji_cake">Kuko</string> + <string name="verification_emoji_heart">Koro</string> + <string name="verification_emoji_smiley">Mieneto</string> + <string name="verification_emoji_robot">Roboto</string> + <string name="verification_emoji_hat">Ĉapelo</string> + <string name="verification_emoji_glasses">Okulvitroj</string> + <string name="verification_emoji_wrench">Boltilo</string> + <string name="verification_emoji_santa">Kristnaska viro</string> + <string name="verification_emoji_thumbsup">Dikfingro supren</string> + <string name="verification_emoji_umbrella">Ombrelo</string> + <string name="verification_emoji_hourglass">SablohorloÄo</string> + <string name="verification_emoji_clock">HorloÄo</string> + <string name="verification_emoji_gift">Donaco</string> + <string name="verification_emoji_lightbulb">Lampo</string> + <string name="verification_emoji_book">Libro</string> + <string name="verification_emoji_pencil">Grifelo</string> + <string name="verification_emoji_paperclip">Paperkuntenilo</string> + <string name="verification_emoji_scissors">Tondilo</string> + <string name="verification_emoji_lock">Seruro</string> + <string name="verification_emoji_key">Åœlosilo</string> + <string name="verification_emoji_hammer">Martelo</string> + <string name="verification_emoji_telephone">Telefono</string> + <string name="verification_emoji_flag">Flago</string> + <string name="verification_emoji_train">Vagonaro</string> + <string name="verification_emoji_bicycle">Biciklo</string> + <string name="verification_emoji_airplane">Aviadilo</string> + <string name="verification_emoji_rocket">Raketo</string> + <string name="verification_emoji_trophy">Trofeo</string> + <string name="verification_emoji_ball">Pilko</string> + <string name="verification_emoji_guitar">Gitaro</string> + <string name="verification_emoji_trumpet">Trumpeto</string> + <string name="verification_emoji_bell">Sonorilo</string> + <string name="verification_emoji_anchor">Ankro</string> + <string name="verification_emoji_headphone">KapaÅdilo</string> + <string name="verification_emoji_folder">Dosierujo</string> + <string name="verification_emoji_pin">Pinglo</string> + + <string name="initial_sync_start_importing_account">Komenca spegulado: +\nEnportante konton…</string> + <string name="initial_sync_start_importing_account_crypto">Komenca spegulado: +\nEnportante ĉifrilojn</string> + <string name="initial_sync_start_importing_account_rooms">Komenca spegulado: +\nEnportante ĉambrojn</string> + <string name="initial_sync_start_importing_account_joined_rooms">Komenca spegulado: +\nEnportante aliÄitajn ĉambrojn</string> + <string name="initial_sync_start_importing_account_invited_rooms">Komenca spegulado: +\nEnportante ĉambrojn de invitoj</string> + <string name="initial_sync_start_importing_account_left_rooms">Komenca spegulado: +\nEnportante forlasitajn ĉambrojn</string> + <string name="initial_sync_start_importing_account_groups">Komenca spegulado: +\nEnportante komunumojn</string> + <string name="initial_sync_start_importing_account_data">Komenca spegulado: +\nEnportante datumojn de konto</string> + + <string name="event_status_sending_message">Sendante mesaÄon…</string> + <string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string> + + <string name="notice_requested_voip_conference">%1$s petis grupan vokon</string> + <string name="notice_voip_started">Grupa voko komenciÄis</string> + <string name="notice_voip_finished">Grupa voko finiÄis</string> + + <string name="notice_avatar_changed_too">(ankaÅ profilbildo ÅanÄiÄis)</string> + <string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string> + <string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string> + <string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string> + <string name="notice_room_join_with_reason">%1$s aliÄis al la ĉambro. Kialo: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton al %2$s. Kialo: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item> + <item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item> + <item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string> + + <string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string> + + <string name="notice_room_guest_access_can_join">%1$s permesis al gastoj aliÄi al la ĉambro.</string> + <string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj aliÄi al la ĉambro.</string> + + <string name="notice_end_to_end_ok">%1$s Åaltis tutvojan ĉifradon.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s Åaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string> + + <string name="key_verification_request_fallback_message">%s petas kontrolon de via Ålosilo, sed via kliento ne subtenas kontrolon de Ålosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de Ålosiloj.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..35b7bfc829d3caead13723506787fd3eae80e970 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,96 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s envió una imagen.</string> + + <string name="notice_room_invite_no_invitee">la invitación de %s</string> + <string name="notice_room_invite">%1$s invitó a %2$s</string> + <string name="notice_room_invite_you">%1$s te invitó</string> + <string name="notice_room_join">%1$s se unió</string> + <string name="notice_room_leave">%1$s salió</string> + <string name="notice_room_reject">%1$s rechazó la invitación</string> + <string name="notice_room_kick">%1$s quitó a %2$s</string> + <string name="notice_room_unban">%1$s desprohibió a %2$s</string> + <string name="notice_room_ban">%1$s prohibió %2$s</string> + <string name="notice_room_withdraw">%1$s retiró la invitación de %2$s</string> + <string name="notice_avatar_url_changed">%1$s cambió su foto de perfil</string> + <string name="notice_display_name_set">%1$s estableció %2$s como su nombre visible</string> + <string name="notice_display_name_changed_from">%1$s cambió su nombre visible de %2$s a %3$s</string> + <string name="notice_display_name_removed">%1$s eliminó su nombre visible (%2$s)</string> + <string name="notice_room_topic_changed">%1$s cambió el tema a: %2$s</string> + <string name="notice_room_name_changed">%1$s cambió el nombre de la sala a: %2$s</string> + <string name="notice_placed_video_call">%s comenzó una llamada de video.</string> + <string name="notice_placed_voice_call">%s comenzó una llamada de voz.</string> + <string name="notice_answered_call">%s recibió la llamada.</string> + <string name="notice_ended_call">%s terminó la llamada.</string> + <string name="notice_made_future_room_visibility">%1$s dejó que %2$s vea el historial del futuro</string> + <string name="notice_room_visibility_invited">todos los miembros de la sala, desde su invitación.</string> + <string name="notice_room_visibility_joined">todos los miembros de la sala, desde cuando entraron.</string> + <string name="notice_room_visibility_shared">todos los miembros de la sala.</string> + <string name="notice_room_visibility_world_readable">todos.</string> + <string name="notice_room_visibility_unknown">desconocido (%s).</string> + <string name="notice_end_to_end">%1$s encendió el cifrado de extremo a extremo (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s solicitó una conferencia VoIP</string> + <string name="notice_voip_started">conferencia VoIP comenzó</string> + <string name="notice_voip_finished">conferencia VoIP finalizó</string> + + <string name="notice_avatar_changed_too">(foto de perfil también se cambió)</string> + <string name="notice_room_name_removed">%1$s eliminó el nombre de la sala</string> + <string name="notice_room_topic_removed">%1$s retiró el tema de la sala</string> + <string name="notice_profile_change_redacted">%1$s actualizó su perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s envió una invitación a %2$s para entrar a la sala</string> + <string name="notice_room_third_party_registered_invite">%1$s aceptó la invitación de %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** No se puede descifrar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">El dispositivo del remitente no nos ha enviado las claves de este mensaje.</string> + + <!-- Room Screen --> + <string name="could_not_redact">No se pudo redactar</string> + <string name="unable_to_send_message">No se puede enviar el mensaje</string> + + <string name="message_failed_to_upload">La subida de la imagen falló</string> + + <!-- general errors --> + <string name="network_error">Error de la red</string> + <string name="matrix_error">Error de Matrix</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">No es posible volver a unirse a una sala vacÃa.</string> + + <string name="encrypted_message">Mensaje cifrado</string> + + <!-- medium friendly name --> + <string name="medium_email">Correo electrónico</string> + <string name="medium_phone_number">Número telefónico</string> + + <string name="summary_user_sent_sticker">%1$s envió una calcomanÃa.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Invitación de %s</string> + <string name="room_displayname_room_invite">Invitación de Sala</string> + <string name="room_displayname_two_members">%1$s y %2$s</string> + <string name="room_displayname_empty_room">Sala vacÃa</string> + + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s y otro</item> + <item quantity="other">%1$s y %2$d otros</item> + </plurals> + + <string name="notice_event_redacted">Mensaje eliminado</string> + <string name="notice_event_redacted_by">Mensaje eliminado por %1$s</string> + <string name="notice_event_redacted_with_reason">Mensaje eliminado [motivo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mensaje eliminado por %1$s [motivo: %2$s]</string> + <string name="verification_emoji_dog">Perro</string> + <string name="verification_emoji_cat">Gato</string> + <string name="verification_emoji_lion">León</string> + <string name="verification_emoji_horse">Caballo</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3c019b3b80935e7953bda2202e554a6ae52be429 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -0,0 +1,215 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s envió una imagen.</string> + + <string name="notice_room_invite_no_invitee">la invitación de %s</string> + <string name="notice_room_invite">%1$s invitó a %2$s</string> + <string name="notice_room_invite_you">%1$s te ha invitado</string> + <string name="notice_room_join">%1$s se ha unido</string> + <string name="notice_room_leave">%1$s salió</string> + <string name="notice_room_reject">%1$s rechazó la invitación</string> + <string name="notice_room_kick">%1$s expulsó a %2$s</string> + <string name="notice_room_unban">%1$s le quitó el veto a %2$s</string> + <string name="notice_room_ban">%1$s vetó a %2$s</string> + <string name="notice_room_withdraw">%1$s retiró la invitación de %2$s</string> + <string name="notice_avatar_url_changed">%1$s cambió su avatar</string> + <string name="notice_display_name_set">%1$s estableció %2$s como su nombre público</string> + <string name="notice_display_name_changed_from">%1$s cambió su nombre público de %2$s a %3$s</string> + <string name="notice_display_name_removed">%1$s eliminó su nombre público (%2$s)</string> + <string name="notice_room_topic_changed">%1$s cambió el tema a: %2$s</string> + <string name="notice_room_name_changed">%1$s cambió el nombre de la sala a: %2$s</string> + <string name="notice_placed_video_call">%s realizó una llamada de vÃdeo.</string> + <string name="notice_placed_voice_call">%s realizó una llamada de voz.</string> + <string name="notice_answered_call">%s contestó la llamada.</string> + <string name="notice_ended_call">%s finalizó la llamada.</string> + <string name="notice_made_future_room_visibility">%1$s hizo visible el historial futuro de la sala para %2$s</string> + <string name="notice_room_visibility_invited">todos los miembros de la sala, desde su invitación.</string> + <string name="notice_room_visibility_joined">todos los miembros de la sala, desde el momento en que se unieron.</string> + <string name="notice_room_visibility_shared">todos los miembros de la sala.</string> + <string name="notice_room_visibility_world_readable">todos.</string> + <string name="notice_room_visibility_unknown">desconocido (%s).</string> + <string name="notice_end_to_end">%1$s activó el cifrado de extremo a extremo (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s solicitó una conferencia de vozIP</string> + <string name="notice_voip_started">conferencia de vozIP iniciada</string> + <string name="notice_voip_finished">conferencia de vozIP finalizada</string> + + <string name="notice_avatar_changed_too">(el avatar también se cambió)</string> + <string name="notice_room_name_removed">%1$s eliminó el nombre de la sala</string> + <string name="notice_room_topic_removed">%1$s eliminó el tema de la sala</string> + <string name="notice_profile_change_redacted">%1$s actualizó su perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s invitó a %2$s a unirse a la sala</string> + <string name="notice_room_third_party_registered_invite">%1$s aceptó la invitación para %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** No es posible descifrar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">El dispositivo emisor no nos ha enviado las claves para este mensaje.</string> + + <!-- Room Screen --> + <string name="could_not_redact">No se pudo redactar</string> + <string name="unable_to_send_message">No es posible enviar el mensaje</string> + + <string name="message_failed_to_upload">No se pudo cargar la imagen</string> + + <!-- general errors --> + <string name="network_error">Error de red</string> + <string name="matrix_error">Error de Matrix</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Actualmente no es posible volver a unirse a una sala vacÃa.</string> + + <string name="encrypted_message">Mensaje cifrado</string> + + <!-- medium friendly name --> + <string name="medium_email">Dirección de correo electrónico</string> + <string name="medium_phone_number">Número telefónico</string> + + <string name="summary_user_sent_sticker">%1$s envió una pegatina.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Invitación de %s</string> + <string name="room_displayname_room_invite">Invitación a Sala</string> + <string name="room_displayname_two_members">%1$s y %2$s</string> + <string name="room_displayname_empty_room">Sala vacÃa</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s y 1 otro</item> + <item quantity="other">%1$s y %2$d otros</item> + </plurals> + + + <string name="notice_event_redacted">Mensaje eliminado</string> + <string name="notice_event_redacted_by">Mensaje eliminado por %1$s</string> + <string name="notice_event_redacted_with_reason">Mensaje eliminado [motivo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mensaje eliminado por %1$s [motivo: %2$s]</string> + <string name="notice_room_third_party_revoked_invite">%1$s ha revocado la invitación a unirse a la sala para %2$s</string> + <string name="verification_emoji_dog">Perro</string> + <string name="verification_emoji_cat">Gato</string> + <string name="verification_emoji_lion">León</string> + <string name="verification_emoji_horse">Caballo</string> + <string name="verification_emoji_unicorn">Unicornio</string> + <string name="verification_emoji_pig">Cerdo</string> + <string name="verification_emoji_elephant">Elefante</string> + <string name="verification_emoji_rabbit">Conejo</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Gallo</string> + <string name="verification_emoji_penguin">Pingüino</string> + <string name="verification_emoji_turtle">Tortuga</string> + <string name="verification_emoji_fish">Pez</string> + <string name="verification_emoji_octopus">Pulpo</string> + <string name="verification_emoji_butterfly">Mariposa</string> + <string name="verification_emoji_flower">Flor</string> + <string name="verification_emoji_tree">Ãrbol</string> + <string name="verification_emoji_cactus">Cactus</string> + <string name="verification_emoji_mushroom">Seta</string> + <string name="verification_emoji_moon">Luna</string> + <string name="verification_emoji_cloud">Nube</string> + <string name="verification_emoji_fire">Fuego</string> + <string name="verification_emoji_banana">Plátano</string> + <string name="verification_emoji_apple">Manzana</string> + <string name="verification_emoji_strawberry">Fresa</string> + <string name="verification_emoji_corn">MaÃz</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Pastel</string> + <string name="verification_emoji_heart">Corazón</string> + <string name="verification_emoji_hat">Sombrero</string> + <string name="verification_emoji_glasses">Gafas</string> + <string name="verification_emoji_wrench">Llave inglesa</string> + <string name="verification_emoji_thumbsup">Pulgares arriba</string> + <string name="verification_emoji_umbrella">Paraguas</string> + <string name="verification_emoji_hourglass">Reloj de arena</string> + <string name="verification_emoji_clock">Reloj</string> + <string name="verification_emoji_gift">Regalo</string> + <string name="verification_emoji_lightbulb">Bombilla</string> + <string name="verification_emoji_book">Libro</string> + <string name="verification_emoji_pencil">Lápiz</string> + <string name="verification_emoji_paperclip">Clip</string> + <string name="verification_emoji_scissors">Tijeras</string> + <string name="verification_emoji_lock">Candado</string> + <string name="verification_emoji_key">Llave</string> + <string name="verification_emoji_hammer">Martillo</string> + <string name="verification_emoji_telephone">Teléfono</string> + <string name="verification_emoji_flag">Bandera</string> + <string name="verification_emoji_train">Tren</string> + <string name="verification_emoji_bicycle">Bicicleta</string> + <string name="verification_emoji_airplane">Avión</string> + <string name="verification_emoji_rocket">Cohete</string> + <string name="verification_emoji_trophy">Trofeo</string> + <string name="verification_emoji_ball">Pelota</string> + <string name="verification_emoji_guitar">Guitarra</string> + <string name="verification_emoji_trumpet">Trompeta</string> + <string name="verification_emoji_bell">Campana</string> + <string name="verification_emoji_anchor">Ancla</string> + <string name="verification_emoji_headphone">Auriculares</string> + <string name="verification_emoji_folder">Carpeta</string> + <string name="initial_sync_start_importing_account">Sincronización Inicial +\nImportando cuenta…</string> + <string name="initial_sync_start_importing_account_rooms">Sincronización Inicial: +\nImportando Salas</string> + <string name="initial_sync_start_importing_account_groups">Sincronización Inicial: +\nImportando Comunidades</string> + <string name="initial_sync_start_importing_account_data">Sincronización Inicial: +\nImportando Datos de la Cuenta</string> + + <string name="event_status_sending_message">Enviando mensaje…</string> + <string name="clear_timeline_send_queue">Borrar cola de envÃo</string> + + <string name="notice_room_invite_with_reason">%1$s ha invitado a %2$s. Razón: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s te ha invitado. Razón: %2$s</string> + <string name="notice_room_join_with_reason">%1$s se ha unido. Razón: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s se ha ido. Razón: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s ha rechadazo la invitación. Razón: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s expulsó a %2$s. Razón: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s ha baneado a %2$s. Razón: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s ha aceptado la invitación para %2$s. Razón: %3$s</string> + <string name="notice_room_canonical_alias_unset">%1$s ha eliminado la dirección principal para esta sala.</string> + + <string name="notice_room_update">%s ha actualizado la sala.</string> + + <string name="verification_emoji_globe">Globo Terráqueo</string> + <string name="verification_emoji_smiley">Cara sonriente</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_santa">Papá Noel</string> + <string name="verification_emoji_pin">Pin</string> + + <string name="initial_sync_start_importing_account_crypto">Sincronización Inicial: +\nImportando criptografÃa</string> + <string name="initial_sync_start_importing_account_joined_rooms">Sincronización Inicial: +\nImportando Salas a las que te has unido</string> + <string name="initial_sync_start_importing_account_invited_rooms">Sincronización Inicial: +\nImportando Salas a las que has sido invitada</string> + <string name="initial_sync_start_importing_account_left_rooms">Sincronización Inicial: +\nImportando Salas Abandonadas</string> + <string name="notice_room_invite_no_invitee_with_reason">Invitación de %1$s. Razón: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s ha desbaneado a %2$s. Razón: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s envió una invitación a %2$s para que se una a la sala. Razón: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s revocó la invitación de %2$s para unirse a la sala. Razón: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s ha retirado la invitación de %2$s. Razón: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s ha añadido %2$s como alias de esta sala.</item> + <item quantity="other">%1$s ha añadido %2$s como alias de esta sala.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s ha quitado %2$s como alias de esta habitación.</item> + <item quantity="other">%1$s ha quitado %2$s como alias de esta habitación.</item> + </plurals> + + <string name="notice_room_canonical_alias_set">%1$s ha establecido la dirección principal de esta sala a %2$s.</string> + <string name="notice_room_guest_access_can_join">%1$s ha permitido que los invitados se unan a la sala.</string> + <string name="notice_room_guest_access_forbidden">%1$s ha impedido que los invitados se unan a la sala.</string> + + <string name="notice_end_to_end_ok">%1$s ha activado la encriptación extremo a extremo.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s).</string> + + <string name="key_verification_request_fallback_message">%s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..657d5446eb55059669f46b535e38f27f5347b54d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -0,0 +1,302 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s saatis pildi.</string> + <string name="summary_user_sent_sticker">%1$s saatis kleepsu.</string> + + <string name="notice_room_invite_no_invitee">Kasutaja %s kutse</string> + <string name="notice_room_invite">%1$s kutsus kasutajat %2$s</string> + <string name="notice_room_invite_you">%1$s kutsus sind</string> + <string name="notice_room_join">%1$s liitus jututoaga</string> + <string name="notice_room_leave">%1$s lahkus jututoast</string> + <string name="notice_room_reject">%1$s lükkas tagasi kutse</string> + <string name="notice_room_kick">%1$s müksas kasutajat %2$s</string> + <string name="notice_room_withdraw">%1$s võttis tagasi kutse kasutajale %2$s</string> + <string name="notice_avatar_url_changed">%1$s muutis oma avatari</string> + <string name="notice_display_name_set">%1$s määras oma kuvatavaks nimeks %2$s</string> + <string name="notice_display_name_changed_from">%1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s</string> + <string name="notice_display_name_removed">%1$s eemaldas oma kuvatava nime (%2$s)</string> + <string name="notice_room_topic_changed">%1$s muutis uueks teemaks %2$s</string> + <string name="notice_room_name_changed">%1$s muutis jututoa uueks nimeks %2$s</string> + <string name="notice_placed_video_call">%s alustas videokõnet.</string> + <string name="notice_placed_voice_call">%s alustas häälkõnet.</string> + <string name="notice_answered_call">%s vastas kõnele.</string> + <string name="notice_ended_call">%s lõpetas kõne.</string> + <string name="notice_made_future_room_visibility">%1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s</string> + <string name="notice_room_visibility_invited">kõikidele jututoa liikmetele alates kutsumise hetkest.</string> + <string name="notice_room_visibility_joined">kõikidele jututoa liikmetele alates liitumise hetkest.</string> + <string name="notice_room_visibility_shared">kõikidele jututoa liikmetele.</string> + <string name="notice_room_visibility_world_readable">kõikidele.</string> + <string name="notice_room_visibility_unknown">teadmata (%s).</string> + <string name="notice_end_to_end">%1$s lülitas sisse läbiva krüptimise (%2$s)</string> + <string name="notice_room_update">%s uuendas seda jututuba.</string> + + <string name="notice_requested_voip_conference">%1$s saatis VoIP konverentsi kutse</string> + <string name="notice_voip_started">VoIP-konverents algas</string> + <string name="notice_voip_finished">VoIP-konverents lõppes</string> + + <string name="notice_avatar_changed_too">(samuti sai avatar muudetud)</string> + <string name="notice_room_name_removed">%1$s eemaldas jututoa nime</string> + <string name="notice_room_topic_removed">%1$s eemaldas jututoa teema</string> + <string name="notice_event_redacted">Sõnum on eemaldatud</string> + <string name="notice_event_redacted_by">Sõnum on eemaldatud %1$s poolt</string> + <string name="notice_event_redacted_with_reason">Sõnum on eemaldatud [põhjus: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Sõnum on eemaldatud %1$s poolt [põhjus: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s uuendas oma profiili %2$s</string> + <string name="notice_room_third_party_invite">%1$s saatis jututoaga liitumiseks kutse kasutajale %2$s</string> + <string name="notice_room_third_party_revoked_invite">%1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s võttis vastu kutse %2$s nimel</string> + + <string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string> + + <string name="could_not_redact">Ei saanud muuta sõnumit</string> + <string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string> + + <string name="message_failed_to_upload">Faili üles laadimine ei õnnestunud</string> + + <string name="network_error">Võrguühenduse viga</string> + <string name="matrix_error">Matrix\'i viga</string> + + <string name="room_error_join_failed_empty_room">Hetkel ei ole võimalik uuesti liituda tühja jututoaga.</string> + + <string name="encrypted_message">Krüptitud sõnum</string> + + <string name="medium_email">E-posti aadress</string> + <string name="medium_phone_number">Telefoninumber</string> + + <string name="room_displayname_invite_from">Kutse kasutajalt %s</string> + <string name="room_displayname_room_invite">Kutse jututuppa</string> + + <string name="room_displayname_two_members">%1$s ja %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s ja üks muu</item> + <item quantity="other">%1$s ja %2$d muud</item> + </plurals> + + <string name="room_displayname_empty_room">Tühi jututuba</string> + + + <string name="verification_emoji_dog">Koer</string> + <string name="verification_emoji_cat">Kass</string> + <string name="verification_emoji_lion">Lõvi</string> + <string name="verification_emoji_horse">Hobune</string> + <string name="verification_emoji_unicorn">Ãœkssarvik</string> + <string name="verification_emoji_pig">Siga</string> + <string name="verification_emoji_elephant">Elevant</string> + <string name="verification_emoji_rabbit">Jänes</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kukk</string> + <string name="verification_emoji_penguin">Pingviin</string> + <string name="verification_emoji_turtle">Kilpkonn</string> + <string name="verification_emoji_fish">Kala</string> + <string name="verification_emoji_octopus">Kaheksajalg</string> + <string name="verification_emoji_butterfly">Liblikas</string> + <string name="verification_emoji_flower">Lill</string> + <string name="verification_emoji_tree">Puu</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Seen</string> + <string name="verification_emoji_globe">Maakera</string> + <string name="verification_emoji_moon">Kuu</string> + <string name="verification_emoji_cloud">Pilv</string> + <string name="verification_emoji_fire">Tuli</string> + <string name="verification_emoji_banana">Banaan</string> + <string name="verification_emoji_apple">Õun</string> + <string name="verification_emoji_strawberry">Maasikas</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_pizza">Pitsa</string> + <string name="verification_emoji_cake">Kook</string> + <string name="verification_emoji_heart">Süda</string> + <string name="verification_emoji_smiley">Smaili</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Müts</string> + <string name="verification_emoji_glasses">Prillid</string> + <string name="verification_emoji_wrench">Mutrivõti</string> + <string name="verification_emoji_santa">Jõuluvana</string> + <string name="verification_emoji_thumbsup">Pöidlad püsti</string> + <string name="verification_emoji_umbrella">Vihmavari</string> + <string name="verification_emoji_hourglass">Liivakell</string> + <string name="verification_emoji_clock">Kell</string> + <string name="verification_emoji_gift">Kingitus</string> + <string name="verification_emoji_lightbulb">Lambipirn</string> + <string name="verification_emoji_book">Raamat</string> + <string name="verification_emoji_pencil">Pliiats</string> + <string name="verification_emoji_paperclip">Kirjaklamber</string> + <string name="verification_emoji_scissors">Käärid</string> + <string name="verification_emoji_lock">Lukk</string> + <string name="verification_emoji_key">Võti</string> + <string name="verification_emoji_hammer">Haamer</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Lipp</string> + <string name="verification_emoji_train">Rong</string> + <string name="verification_emoji_bicycle">Jalgratas</string> + <string name="verification_emoji_airplane">Lennuk</string> + <string name="verification_emoji_rocket">Rakett</string> + <string name="verification_emoji_trophy">Auhind</string> + <string name="verification_emoji_ball">Pall</string> + <string name="verification_emoji_guitar">Kitarr</string> + <string name="verification_emoji_trumpet">Trompet</string> + <string name="verification_emoji_bell">Kelluke</string> + <string name="verification_emoji_anchor">Ankur</string> + <string name="verification_emoji_headphone">Kõrvaklapid</string> + <string name="verification_emoji_folder">Kaust</string> + <string name="verification_emoji_pin">Knopka</string> + + <string name="initial_sync_start_importing_account">Alglaadimine: +\nImpordin kontot…</string> + <string name="initial_sync_start_importing_account_crypto">Alglaadimine: +\nImpordin krüptoseadistusi</string> + <string name="initial_sync_start_importing_account_rooms">Alglaadimine: +\nImpordin jututubasid</string> + <string name="initial_sync_start_importing_account_joined_rooms">Alglaadimine: +\nImpordin liitutud jututubasid</string> + <string name="initial_sync_start_importing_account_invited_rooms">Alglaadimine: +\nImpordin kutsutud jututubasid</string> + <string name="initial_sync_start_importing_account_left_rooms">Alglaadimine: +\nImpordin lahkutud jututubasid</string> + <string name="initial_sync_start_importing_account_groups">Alglaadimine: +\nImpordin kogukondi</string> + <string name="initial_sync_start_importing_account_data">Alglaadimine: +\nImpordin kontoandmeid</string> + + <string name="event_status_sending_message">Saadan sõnumit…</string> + <string name="clear_timeline_send_queue">Tühjenda saatmisjärjekord</string> + + <string name="notice_room_invite_no_invitee_with_reason">Kasutaja %1$s kutse. Põhjus: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s kutsus kasutajat %2$s. Põhjus: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s kutsus sind. Põhjus: %2$s</string> + <string name="notice_room_join_with_reason">%1$s liitus jututoaga. Põhjus: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s lahkus jututoast. Põhjus: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s lükkas kutse tagasi. Põhjus: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s müksas välja kasutaja %2$s. Põhjus: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s</string> + + <string name="notice_end_to_end_ok">%1$s lülitas sisse läbiva krüptimise.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s).</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s lisas %2$s selle jututoa aadressiks.</item> + <item quantity="other">%1$s lisas %2$s selle jututoa aadressideks.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s eemaldas %2$s kui selle jututoa aadressi.</item> + <item quantity="other">%1$s eemaldas %2$s selle jututoa aadresside hulgast.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s lisas %2$s ja eemaldas %3$s selle jututoa aadresside loendist.</string> + + <string name="notice_room_canonical_alias_set">%1$s seadistas selle jututoa põhiaadressiks %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s eemaldas selle jututoa põhiaadressi.</string> + + <string name="notice_room_guest_access_can_join">%1$s lubas külalistel selle jututoaga liituda.</string> + <string name="notice_room_guest_access_forbidden">%1$s seadistas, et külalised ei või selle jututoaga liituda.</string> + + <string name="key_verification_request_fallback_message">%s soovib verifitseerida sinu võtmeid, kuid sinu kasutatav klient ei oska vestluse-sisest verifitseerimist teha. Sa pead kasutama traditsioonilist verifitseerimislahendust.</string> + + <string name="notice_room_created">Kasutaja %1$s lõi jututoa</string> + <string name="summary_you_sent_image">Sina saatsid pildi.</string> + <string name="summary_you_sent_sticker">Sina saatsid kleepsu.</string> + + <string name="notice_room_invite_no_invitee_by_you">Sinu kutse</string> + <string name="notice_room_created_by_you">Sa lõid jututoa</string> + <string name="notice_room_invite_by_you">Sina kutsusid kasutajat %1$s</string> + <string name="notice_room_join_by_you">Sina liitusid jututoaga</string> + <string name="notice_room_leave_by_you">Sina lahkusid jututoast</string> + <string name="notice_room_reject_by_you">Sina lükkasid kutse tagasi</string> + <string name="notice_room_kick_by_you">Sina müksasid %1$s välja</string> + <string name="notice_room_unban">%1$s taastas %2$s ligipääsu</string> + <string name="notice_room_unban_by_you">Sina taastasid %1$s ligipääsu</string> + <string name="notice_room_ban">%1$s keelas %1$s ligipääsu</string> + <string name="notice_room_ban_by_you">Sina keelasid %1$s ligipääsu</string> + <string name="notice_room_withdraw_by_you">Sina võtsid tagasi %1$s kutse</string> + <string name="notice_avatar_url_changed_by_you">Sina muutsid oma tunnuspilti</string> + <string name="notice_display_name_set_by_you">Sina määrasid oma kuvatavaks nimeks %1$s</string> + <string name="notice_display_name_changed_from_by_you">Sina muutsid senise kuvatava nime %1$s uueks nimeks %2$s</string> + <string name="notice_display_name_removed_by_you">Sina eemaldasid oma kuvatava nime (oli %1$s)</string> + <string name="notice_room_topic_changed_by_you">Sina muutsid uueks teemaks %1$s</string> + <string name="notice_room_avatar_changed">%1$s muutis jututoa tunnuspilti</string> + <string name="notice_room_avatar_changed_by_you">Sina muutsid jututoa tunnuspilti</string> + <string name="notice_room_name_changed_by_you">Sina muutsid jututoa uueks nimeks %1$s</string> + <string name="notice_placed_video_call_by_you">Sa alustasid videokõnet.</string> + <string name="notice_placed_voice_call_by_you">Sa alustasid häälkõnet.</string> + <string name="notice_call_candidates">%s saatis info kõne algatamiseks.</string> + <string name="notice_call_candidates_by_you">Sa saatsid info kõne algatamiseks.</string> + <string name="notice_answered_call_by_you">Sa vastasid kõnele.</string> + <string name="notice_ended_call_by_you">Sa lõpetasid kõne.</string> + <string name="notice_made_future_room_visibility_by_you">Sa seadistasid, et tulevane jututoa ajalugu on nähtav kasutajale %1$s</string> + <string name="notice_end_to_end_by_you">Sa lülitasid sisse läbiva krüptimise (%1$s)</string> + <string name="notice_room_update_by_you">Sa uuendasid seda jututuba.</string> + + <string name="notice_requested_voip_conference_by_you">Sa algatasid VoIP rühmakõne</string> + <string name="notice_room_name_removed_by_you">Sa eemaldasid jututoa nime</string> + <string name="notice_room_topic_removed_by_you">Sa eemaldasid jututoa teema</string> + <string name="notice_room_avatar_removed">%1$s eemaldas jututoa tunnuspildi</string> + <string name="notice_room_avatar_removed_by_you">Sa eemaldasid jututoa tunnuspildi</string> + <string name="notice_profile_change_redacted_by_you">Sa uuendasid oma profiili %1$s</string> + <string name="notice_room_third_party_invite_by_you">Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks</string> + <string name="notice_room_third_party_revoked_invite_by_you">Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %1$s</string> + <string name="notice_room_third_party_registered_invite_by_you">Sina võtsid vastu kutse %1$s nimel</string> + + <string name="notice_widget_added">%1$s lisas %2$s vidina</string> + <string name="notice_widget_added_by_you">Sina lisasid %1$s vidina</string> + <string name="notice_widget_removed">%1$s eemaldas %2$s vidina</string> + <string name="notice_widget_removed_by_you">Sina eemdaldasid %1$s vidina</string> + <string name="notice_widget_modified">%1$s muutis %2$s vidinat</string> + <string name="notice_widget_modified_by_you">Sa muutsid %1$s vidinat</string> + + <string name="power_level_admin">Peakasutaja</string> + <string name="power_level_moderator">Moderaator</string> + <string name="power_level_default">Tavakasutaja</string> + <string name="power_level_custom">Kohandatud kasutajaõigused (%1$s)</string> + <string name="power_level_custom_no_value">Kohandatud õigused</string> + + <string name="notice_power_level_changed_by_you">Sina muutsid kasutaja %1$s õigusi.</string> + <string name="notice_power_level_changed">%1$s muutis kasutaja %2$s õigusi.</string> + <string name="notice_power_level_diff">%1$s õiguste muutus %2$s -> %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">Sinu kutse. Põhjus %1$s</string> + <string name="notice_room_invite_with_reason_by_you">Sina kutsusid kasutajat %1$s. Põhjus: %1$s</string> + <string name="notice_room_join_with_reason_by_you">Sina liitusid jututoaga. Põhjus: %1$s</string> + <string name="notice_room_leave_with_reason_by_you">Sina lahkusid jututoast. Põhjus: %1$s</string> + <string name="notice_room_reject_with_reason_by_you">Sina lükkasid kutse tagasi. Põhjus: %1$s</string> + <string name="notice_room_kick_with_reason_by_you">Sina müksasid kasutaja %1$s välja. Põhjus: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s taastas ligipääsu kasutajale %2$s. Põhjus: %3$s</string> + <string name="notice_room_unban_with_reason_by_you">Sina taastasid kasutaja %1$s ligipääsu. Põhjus: %2$s</string> + <string name="notice_room_ban_with_reason">%1$s keelas kasutaja %2$s ligipääsu. Põhjus: %3$s</string> + <string name="notice_room_ban_with_reason_by_you">Sina keelasid kasutaja %1$s ligipääsu. Põhjus: %2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks. Põhjus: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %1$s. Põhjus: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Sina võtsid vastu kutse %1$s nimel. Põhjus: %2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">Sina võtsid tagasi kasutaja %1$s kutse. Põhjus: %2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">Sina lisasid %1$s selle jututoa aadressiks.</item> + <item quantity="other">Sina lisasid %1$s selle jututoa aadressideks.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">Sina eemaldasid %1$s, kui selle jututoa aadressi.</item> + <item quantity="other">Sina eemaldasid %1$s selle jututoa aadresside hulgast.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">Sina lisasid %1$s selle jututoa aadressiks ning eemaldasid %2$s aadresside hulgast.</string> + + <string name="notice_room_canonical_alias_set_by_you">Sina seadistasid selle jututoa põhiaadressiks %1$s.</string> + <string name="notice_room_canonical_alias_unset_by_you">Sina eemaldasid selle jututoa põhiaadressi.</string> + + <string name="notice_room_guest_access_can_join_by_you">Sina lubasid külalistel selle jututoaga liituda.</string> + <string name="notice_room_guest_access_forbidden_by_you">Sina seadistasid, et külalised ei või selle jututoaga liituda.</string> + + <string name="notice_end_to_end_ok_by_you">Sa lülitasid sisse läbiva krüptimise.</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Sa lülitasid sisse läbiva krüptimise (kasutusel on tundmatu algoritm %1$s).</string> + + <string name="call_notification_answer">Võta vastu</string> + <string name="call_notification_reject">Keeldu</string> + <string name="call_notification_hangup">Lõpeta kõne</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1a5c81fe5e08818b96f2b7e73a9692c153b5cb9f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -0,0 +1,207 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s erabiltzaileak irudi bat bidali du.</string> + + <string name="notice_room_invite_no_invitee">%s erabiltzailearen gonbidapena</string> + <string name="notice_room_invite">%1$s erabiltzaileak %2$s gonbidatu du</string> + <string name="notice_room_invite_you">%1$s erabiltzaileak gonbidatu zaitu</string> + <string name="notice_room_join">%1$s gelara elkartu da</string> + <string name="notice_room_leave">%1$s gelatik atera da</string> + <string name="notice_room_reject">%1$s erabiltzaileak gonbidapena baztertu du</string> + <string name="notice_room_kick">%1$s erabiltzaileak %2$s kanporatu du</string> + <string name="notice_room_unban">%1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari</string> + <string name="notice_room_ban">%1$s erabiltzaileak %2$s debekatu du</string> + <string name="notice_room_withdraw">%1$s erabiltzaileak %2$s erabiltzailearen gonbidapena atzera bota du</string> + <string name="notice_avatar_url_changed">%1$s erabiltzaileak abatarra aldatu du</string> + <string name="notice_display_name_set">%1$s erabiltzaileak bere pantaila-izena aldatu du beste honetara: %2$s</string> + <string name="notice_display_name_changed_from">%1$s erabiltzaileak bere pantaila-izena aldatu du, honetatik: %2$s honetara: %3$s</string> + <string name="notice_display_name_removed">%1$s erabiltzaileak bere pantaila-izena kendu du (%2$s)</string> + <string name="notice_room_topic_changed">%1$s erabiltzaileak mintzagaia honetara aldatu du: %2$s</string> + <string name="notice_room_name_changed">%1$s erabiltzaileak gelaren izena honetara aldatu du: %2$s</string> + <string name="notice_placed_video_call">%s erabiltzaileak bideo deia hasi du.</string> + <string name="notice_placed_voice_call">%s erabiltzaileak ahots deia hasi du.</string> + <string name="notice_answered_call">%s erabiltzaileak deia erantzun du.</string> + <string name="notice_ended_call">%s erabiltzaileak deia amaitu du.</string> + <string name="notice_made_future_room_visibility">%1$s erabiltzaileak gelaren historiala ikusgai jarri du hauentzat: %2$s</string> + <string name="notice_room_visibility_invited">gelako kide guztiak, gonbidatu zitzaienetik.</string> + <string name="notice_room_visibility_joined">gelako kide guztiak, elkartu zirenetik.</string> + <string name="notice_room_visibility_shared">gelako kide guztiak.</string> + <string name="notice_room_visibility_world_readable">edonor.</string> + <string name="notice_room_visibility_unknown">ezezaguna (%s).</string> + <string name="notice_end_to_end">%1$s erabiltzaileak muturretik muturrera zifratzea aktibatu du (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s erabiltzaileak VoIP konferentzia bat eskatu du</string> + <string name="notice_voip_started">VoIP konferentzia hasita</string> + <string name="notice_voip_finished">VoIP konferentzia amaituta</string> + + <string name="notice_avatar_changed_too">(abatarra ere aldatu da)</string> + <string name="notice_room_name_removed">%1$s erabiltzaileak gelaren izena kendu du</string> + <string name="notice_room_topic_removed">%1$s erabiltzaileak gelaren mintzagaia kendu du</string> + <string name="notice_profile_change_redacted">%1$s erabiltzaileak bere profila eguneratu du %2$s</string> + <string name="notice_room_third_party_invite">%1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari</string> + <string name="notice_room_third_party_registered_invite">%1$s erabiltzaileak %2$s gelarako gonbidapena onartu du</string> + + <string name="notice_crypto_unable_to_decrypt">** Ezin izan da deszifratu: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Igorlearen gailuak ez dizkigu mezu honetarako gakoak bidali.</string> + + <string name="could_not_redact">Ezin izan da kendu</string> + <string name="unable_to_send_message">Ezin izan da mezua bidali</string> + + <string name="message_failed_to_upload">Huts egin du irudia igotzean</string> + + <string name="network_error">Sare errorea</string> + <string name="matrix_error">Matrix errorea</string> + + <string name="room_error_join_failed_empty_room">Ezin da oraingoz hutsik dagoen gela batetara berriro sartu.</string> + + <string name="encrypted_message">Zifratutako mezua</string> + + <string name="medium_email">E-mail helbidea</string> + <string name="medium_phone_number">Telefono zenbakia</string> + + <string name="summary_user_sent_sticker">%1$s erabiltzaileak eranskailu bat bidali du.</string> + + <string name="room_displayname_invite_from">%s gelarako gonbidapena</string> + <string name="room_displayname_room_invite">Gela gonbidapena</string> + <string name="room_displayname_two_members">%1$s eta %2$s</string> + <string name="room_displayname_empty_room">Gela hutsa</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s eta beste bat</item> + <item quantity="other">%1$s eta beste %2$d</item> + </plurals> + + + <string name="notice_event_redacted">Mezua kendu da</string> + <string name="notice_event_redacted_by">%1$s erabiltzaileak mezua kendu du</string> + <string name="notice_event_redacted_with_reason">Mezua kendu da [arrazoia: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">%1$s erabiltzaileak mezua kendu du [arrazoia: %2$s]</string> + <string name="verification_emoji_dog">Txakurra</string> + <string name="verification_emoji_cat">Katua</string> + <string name="verification_emoji_lion">Lehoia</string> + <string name="verification_emoji_horse">Zaldia</string> + <string name="verification_emoji_unicorn">Unikornioa</string> + <string name="verification_emoji_pig">Zerria</string> + <string name="verification_emoji_elephant">Elefantea</string> + <string name="verification_emoji_rabbit">Untxia</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Oilarra</string> + <string name="verification_emoji_penguin">Pinguinoa</string> + <string name="verification_emoji_turtle">Dortoka</string> + <string name="verification_emoji_fish">Arraina</string> + <string name="verification_emoji_octopus">Olagarroa</string> + <string name="verification_emoji_butterfly">Tximeleta</string> + <string name="verification_emoji_flower">Lorea</string> + <string name="verification_emoji_tree">Zuhaitza</string> + <string name="verification_emoji_cactus">Kaktusa</string> + <string name="verification_emoji_mushroom">Perretxikoa</string> + <string name="verification_emoji_globe">Lurra</string> + <string name="verification_emoji_moon">Ilargia</string> + <string name="verification_emoji_cloud">Hodeia</string> + <string name="verification_emoji_fire">Sua</string> + <string name="verification_emoji_banana">Banana</string> + <string name="verification_emoji_apple">Sagarra</string> + <string name="verification_emoji_strawberry">Marrubia</string> + <string name="verification_emoji_corn">Artoa</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Pastela</string> + <string name="verification_emoji_heart">Bihotza</string> + <string name="verification_emoji_smiley">Irrifartxoa</string> + <string name="verification_emoji_robot">Robota</string> + <string name="verification_emoji_hat">Txanoa</string> + <string name="verification_emoji_glasses">Betaurrekoak</string> + <string name="verification_emoji_wrench">Giltza</string> + <string name="verification_emoji_santa">Santa</string> + <string name="verification_emoji_thumbsup">Ederto</string> + <string name="verification_emoji_umbrella">Aterkia</string> + <string name="verification_emoji_hourglass">Harea-erlojua</string> + <string name="verification_emoji_clock">Erlojua</string> + <string name="verification_emoji_gift">Oparia</string> + <string name="verification_emoji_lightbulb">Bonbilla</string> + <string name="verification_emoji_book">Liburua</string> + <string name="verification_emoji_pencil">Arkatza</string> + <string name="verification_emoji_paperclip">Klipa</string> + <string name="verification_emoji_scissors">Artaziak</string> + <string name="verification_emoji_lock">Giltzarrapoa</string> + <string name="verification_emoji_key">Giltza</string> + <string name="verification_emoji_hammer">Mailua</string> + <string name="verification_emoji_telephone">Telefonoa</string> + <string name="verification_emoji_flag">Bandera</string> + <string name="verification_emoji_train">Trena</string> + <string name="verification_emoji_bicycle">Bizikleta</string> + <string name="verification_emoji_airplane">Hegazkina</string> + <string name="verification_emoji_rocket">Kohetea</string> + <string name="verification_emoji_trophy">Saria</string> + <string name="verification_emoji_ball">Baloia</string> + <string name="verification_emoji_guitar">Gitarra</string> + <string name="verification_emoji_trumpet">Tronpeta</string> + <string name="verification_emoji_bell">Kanpaia</string> + <string name="verification_emoji_anchor">Aingura</string> + <string name="verification_emoji_headphone">Aurikularrak</string> + <string name="verification_emoji_folder">Karpeta</string> + <string name="verification_emoji_pin">Txintxeta</string> + + <string name="initial_sync_start_importing_account">Hasierako sinkronizazioa: +\nKontua inportatzen…</string> + <string name="initial_sync_start_importing_account_crypto">Hasierako sinkronizazioa: +\nZifratzea inportatzen</string> + <string name="initial_sync_start_importing_account_rooms">Hasierako sinkronizazioa: +\nGelak inportatzen</string> + <string name="initial_sync_start_importing_account_joined_rooms">Hasierako sinkronizazioa: +\nElkartutako gelak inportatzen</string> + <string name="initial_sync_start_importing_account_invited_rooms">Hasierako sinkronizazioa: +\nGonbidatutako gelak inportatzen</string> + <string name="initial_sync_start_importing_account_left_rooms">Hasierako sinkronizazioa: +\nUtzitako gelak inportatzen</string> + <string name="initial_sync_start_importing_account_groups">Hasierako sinkronizazioa: +\nKomunitateak inportatzen</string> + <string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa: +\nKontuaren datuak inportatzen</string> + + <string name="notice_room_update">%s erabiltzaileak gela hau eguneratu du.</string> + + <string name="event_status_sending_message">Mezua bidaltzen…</string> + <string name="clear_timeline_send_queue">Garbitu bidalketa-ilara</string> + + <string name="notice_room_third_party_revoked_invite">%1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du</string> + <string name="notice_room_invite_no_invitee_with_reason">%1$s erabiltzailearen gonbidapena. Arrazoia: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s erabiltzaileak %2$s gonbidatu du. Arrazoia: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s erabiltzaileak gonbidatu zaitu. Arrazoia: %2$s</string> + <string name="notice_room_join_with_reason">%1$s gelara elkartu da. Arrazoia: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s gelatik atera da. Arrazoia: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s erabiltzaileak gonbidapena baztertu du. Arrazoia: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s erabiltzaileak %2$s kanporatu du. Arrazoia: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari. Arrazoia: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s erabiltzaileak %2$s debekatu du. Arrazoia: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">"%1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari. Arrazoia: %3$s"</string> + <string name="notice_room_third_party_revoked_invite_with_reason">"%1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du. Arrazoia: %3$s"</string> + <string name="notice_room_third_party_registered_invite_with_reason">"%1$s erabiltzaileak %2$s gelarako gonbidapena onartu du. Arrazoia: %3$s"</string> + <string name="notice_room_withdraw_with_reason">"%1$s erabiltzaileak %2$s erabiltzailearen gonbidapena indargabetu du. Arrazoia: %3$s"</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s erabiltzaileak %2$s gehitu du gela honen helbide gisa.</item> + <item quantity="other">%1$s erabiltzaileak %2$s gehitu ditu gela honen helbide gisa.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s erabiltzaileak %2$s kendu du gela honen helbide gisa.</item> + <item quantity="other">%1$s erabiltzaileak %3$s kendu ditu gela honen helbide gisa.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s erabiltzaileak %2$s gehitu %3$s eta kendu ditu gela honen helbide gisa.</string> + + <string name="notice_room_canonical_alias_set">%1$s erabiltzaileak %2$s ezarri du gela honen helbide nagusi gisa.</string> + <string name="notice_room_canonical_alias_unset">%1$s erabiltzaileak gela honen helbide nagusia kendu du.</string> + + <string name="notice_room_guest_access_can_join">%1$k gonbidatuak gelara sartzea onartu du.</string> + <string name="notice_room_guest_access_forbidden">%1%k gonbidatuak gelara sartzea galerazi du.</string> + + <string name="notice_end_to_end_ok">%1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du. (%2$s algoritmo ezezaguna).</string> + + <string name="key_verification_request_fallback_message">%s(e)k zure gakoa egiaztatzea eskatu du, baina zure bezeroak ez du txatean gakoa egiaztatzea onartzen. Gako egiaztaketa zaharra erabili beharko duzu.</string> + + <string name="notice_room_created">%1$s erabiltzaileak gela sortu du</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..18d8578e5426add31c03a90cf4b4b4177bc8c744 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -0,0 +1,206 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s تصویری Ùرستاد.</string> + <string name="summary_user_sent_sticker">%1$s برچسبی Ùرستاد.</string> + + <string name="notice_room_invite_no_invitee">دعوت %s</string> + <string name="notice_room_invite">‫%1$sØŒ %2$s را دعوت کرد</string> + <string name="notice_room_invite_you">%1$s دعوتتان کرد</string> + <string name="notice_room_join">%1$s به اتاق پیوست</string> + <string name="notice_room_leave">%1$s اتاق را ترک کرد</string> + <string name="notice_room_reject">%1$s دعوت را رد کرد</string> + <string name="notice_room_kick">%1$sØŒ %2$s را اخراج کرد</string> + <string name="notice_room_unban">%1$sØŒ انسداد %2$s را رÙع کرد</string> + <string name="notice_room_ban">%1$sØŒ %2$s را مسدود کرد</string> + <string name="notice_room_withdraw">%1$s دعوت %2$s را نپذیرÙت</string> + <string name="notice_avatar_url_changed">%1$s تصویرش را عوض کرد</string> + <string name="notice_display_name_set">%1$s نام نمایشی خود را به %2$s تنظیم کرد</string> + <string name="notice_display_name_changed_from">%1$s نام نمایشیش را از %2$s به %3$s تغییر داد</string> + <string name="notice_display_name_removed">%1$s نام نمایشیش (%2$s) را پاک کرد</string> + <string name="notice_room_topic_changed">%1$s موضوع را به %2$s تغییر داد</string> + <string name="notice_room_name_changed">%1$s نام اتاق را به %2$s تغییر داد</string> + <string name="notice_placed_video_call">%s یک تماس تصویری برقرار کرد.</string> + <string name="notice_placed_voice_call">%s یک تماس صوتی برقرار کرد.</string> + <string name="notice_answered_call">%s تماس را پاسخ داد.</string> + <string name="notice_ended_call">%s به تماس پایان داد.</string> + <string name="notice_made_future_room_visibility">%1$s تاریخچهٔ آیندهٔ اتاق را برای %2$s نمایان کرد</string> + <string name="notice_room_visibility_invited">همهٔ اعضای اتاق، از زمان دعوت شدنشان.</string> + <string name="notice_room_visibility_joined">همهٔ اعضای اتاق، از زمان پیوستنشان.</string> + <string name="notice_room_visibility_shared">همهٔ اعضای اتاق.</string> + <string name="notice_room_visibility_world_readable">هرکسی.</string> + <string name="notice_room_visibility_unknown">ناشناخته (%s).</string> + <string name="notice_end_to_end">%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)</string> + <string name="notice_room_update">%s این اتاق را ارتقا داد.</string> + + <string name="notice_requested_voip_conference">%1$s درخواست یک گردهمایی صوتی داد</string> + <string name="notice_voip_started">گردهمایی صوتی آغاز شد</string> + <string name="notice_voip_finished">گردهمایی صوتی پایان یاÙت</string> + + <string name="notice_avatar_changed_too">(تصویر هم عوض شد)</string> + <string name="notice_room_name_removed">%1$s نام اتاق را پاک کرد</string> + <string name="notice_room_topic_removed">%1$s موضوع اتاق را پاک کرد</string> + <string name="notice_event_redacted">پیام پاک شد</string> + <string name="notice_event_redacted_by">پیام به دست %1$s پاک شد</string> + <string name="notice_event_redacted_with_reason">پیام پاک شد [دلیل: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">پیام به دست %1$s پاک شد [دلیل: %2$s]</string> + <string name="notice_room_third_party_invite">%1$s دعوتی برای پیوستن %2$s به اتاق Ùرستاد</string> + <string name="notice_room_third_party_revoked_invite">%1$s دعوت پیوستن به اتاق %2$s را باطل کرد</string> + <string name="notice_room_third_party_registered_invite">%1$s دعوت برای %2$s را پذیرÙت</string> + + <string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه Ùرستنده، کلیدهای این پیام را برایمان Ù†Ùرستاده است.</string> + + <string name="unable_to_send_message">ناتوان در Ùرستادن پیام</string> + + <string name="message_failed_to_upload">شکست در بارگذاری تصویر</string> + + <string name="network_error">خطای شبکه</string> + <string name="matrix_error">خطای ماتریس</string> + + <string name="room_error_join_failed_empty_room">در Øال Øاضر امکان بازپیوست به اتاقی خالی وجود ندارد‌‌.</string> + + <string name="encrypted_message">پیام رمزنگاری شده</string> + + <string name="medium_email">نشانی رایانامه</string> + <string name="medium_phone_number">شماره تلÙÙ†</string> + + <string name="room_displayname_invite_from">دعوت از %s</string> + <string name="room_displayname_room_invite">دعوت اتاق</string> + + <string name="room_displayname_two_members">%1$s Ùˆ %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s Ùˆ Û± Ù†Ùر دیگر</item> + <item quantity="other">%1$s Ùˆ %2$d Ù†Ùر دیگر</item> + </plurals> + + <string name="room_displayname_empty_room">اتاق خالی</string> + + + <string name="verification_emoji_dog">سگ</string> + <string name="verification_emoji_cat">گربه</string> + <string name="verification_emoji_lion">شیر</string> + <string name="verification_emoji_horse">اسب</string> + <string name="verification_emoji_unicorn">تک‌شاخ</string> + <string name="verification_emoji_pig">خوک</string> + <string name="verification_emoji_elephant">Ùیل</string> + <string name="verification_emoji_rabbit">خرگوش</string> + <string name="verification_emoji_panda">پاندا</string> + <string name="verification_emoji_rooster">خروس</string> + <string name="verification_emoji_penguin">پنگوئن</string> + <string name="verification_emoji_turtle">لاک‌پشت</string> + <string name="verification_emoji_fish">ماهی</string> + <string name="verification_emoji_octopus">هشت‌پا</string> + <string name="verification_emoji_butterfly">پروانه</string> + <string name="verification_emoji_flower">Ú¯Ù„</string> + <string name="verification_emoji_tree">درخت</string> + <string name="verification_emoji_cactus">کاکتوس</string> + <string name="verification_emoji_mushroom">قارچ</string> + <string name="verification_emoji_globe">جهان</string> + <string name="verification_emoji_moon">ماه</string> + <string name="verification_emoji_cloud">ابر</string> + <string name="verification_emoji_fire">آتش</string> + <string name="verification_emoji_banana">موز</string> + <string name="verification_emoji_apple">سیب</string> + <string name="verification_emoji_strawberry">توت‌Ùرنگی</string> + <string name="verification_emoji_corn">بلال</string> + <string name="verification_emoji_pizza">پیتزا</string> + <string name="verification_emoji_cake">کیک</string> + <string name="verification_emoji_heart">قلب</string> + <string name="verification_emoji_smiley">لبخند</string> + <string name="verification_emoji_robot">آدم‌آهنی</string> + <string name="verification_emoji_hat">کلاه</string> + <string name="verification_emoji_glasses">عینک</string> + <string name="verification_emoji_wrench">آچار</string> + <string name="verification_emoji_santa">بابانوئل</string> + <string name="verification_emoji_thumbsup">شست</string> + <string name="verification_emoji_umbrella">چتر</string> + <string name="verification_emoji_hourglass">ساعت شنی</string> + <string name="verification_emoji_clock">ساعت</string> + <string name="verification_emoji_gift">هدیه</string> + <string name="verification_emoji_lightbulb">لامپ</string> + <string name="verification_emoji_book">کتاب</string> + <string name="verification_emoji_pencil">مداد</string> + <string name="verification_emoji_paperclip">گیره کاغذ</string> + <string name="verification_emoji_scissors">قیچی</string> + <string name="verification_emoji_lock">Ù‚ÙÙ„</string> + <string name="verification_emoji_key">کلید</string> + <string name="verification_emoji_hammer">چکّش</string> + <string name="verification_emoji_telephone">تلÙÙ†</string> + <string name="verification_emoji_flag">پرچم</string> + <string name="verification_emoji_train">قطار</string> + <string name="verification_emoji_bicycle">دوچرخه</string> + <string name="verification_emoji_airplane">هواپیما</string> + <string name="verification_emoji_rocket">موشک</string> + <string name="verification_emoji_trophy">جام</string> + <string name="verification_emoji_ball">توپ</string> + <string name="verification_emoji_guitar">گیتار</string> + <string name="verification_emoji_trumpet">ترومپت</string> + <string name="verification_emoji_bell">زنگ</string> + <string name="verification_emoji_anchor">لنگر</string> + <string name="verification_emoji_headphone">هدÙون</string> + <string name="verification_emoji_folder">پوشه</string> + <string name="verification_emoji_pin">پونز</string> + + <string name="initial_sync_start_importing_account">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی Øساب…</string> + <string name="initial_sync_start_importing_account_crypto">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی رمزنگاری</string> + <string name="initial_sync_start_importing_account_rooms">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی اتاق‌ها</string> + <string name="initial_sync_start_importing_account_joined_rooms">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی اتاق‌های پیوسته</string> + <string name="initial_sync_start_importing_account_invited_rooms">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی اتاق‌های دعوت‌شده</string> + <string name="initial_sync_start_importing_account_left_rooms">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی اتاق‌های ترک‌شده</string> + <string name="initial_sync_start_importing_account_groups">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی انجمن‌ها</string> + <string name="initial_sync_start_importing_account_data">همگام‌سازی نخستین: +\nدر Øال درون‌ریزی داده‌های Øساب</string> + + <string name="event_status_sending_message">در Øال Ùرستادن پیام…</string> + <string name="clear_timeline_send_queue">پاک‌سازی صÙ٠در Øال ارسال</string> + + <string name="notice_room_invite_no_invitee_with_reason">دعوت %1$s. دلیل: %2$s</string> + <string name="notice_room_invite_with_reason">%1$sØŒ %2$s را دعوت کرد. دلیل: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s دعوتتان کرد. دلیل: %2$s</string> + <string name="notice_room_join_with_reason">%1$s به اتاق پیوست. دلیل: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s اتاق را ترک کرد. دلیل: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s دعوت را رد کرد. دلیل: %2$s</string> + <string name="notice_room_kick_with_reason">%1$sØŒ %2$s را اخراج کرد. دلیل: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s انسداد %2$s را رÙع کرد. دلیل: %3$s</string> + <string name="notice_room_ban_with_reason">%1$sØŒ %2$s را مسدود کرد. دلیل: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s دعوتی برای پیوستن %2$s به اتاق Ùرستاد. دلیل: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s دعوت برای %2$s را پذیرÙت. دلیل: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s دعوت %2$s را نپذیرÙت. دلیل: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$sØŒ %2$s را به عنوان نشانی‌ای برای این اتاق اÙزود.</item> + <item quantity="other">%1$sØŒ %2$s را به عنوان نشانی‌هایی برای این اتاق اÙزود.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$sØŒ %2$s را به عنوان نشانی‌ای برای این اتاق پاک کرد.</item> + <item quantity="other">%1$sØŒ %3$s را به عنوان نشانی‌هایی برای این اتاق پاک کرد.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s برای نشانی این اتاق، %2$s را اÙزود Ùˆ %3$s را پاک کرد.</string> + + <string name="notice_room_canonical_alias_set">%1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد.</string> + <string name="notice_room_canonical_alias_unset">%1$s نشانی اصلی را برای این اتاق پاک کرد.</string> + + <string name="notice_room_guest_access_can_join">%1$s اجازه داد میمهانان به گروه بپیوندند.</string> + <string name="notice_room_guest_access_forbidden">%1$s جلوی پیوستن میمهانان به گروه را گرÙت.</string> + + <string name="notice_end_to_end_ok">%1$s رمزنگاری سرتاسری را روشن کرد.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیص‌داده‌نشده %2$s ).</string> + + <string name="key_verification_request_fallback_message">%s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون Ú¯Ù¾ را پشتیبانی نمی‌کند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استÙاده کنید.</string> + + <string name="notice_room_created">%1$s اتاق را ایجاد کرد</string> + <string name="notice_profile_change_redacted">%1$s نمایه خود را به‌روز کرد %2$s</string> + <string name="could_not_redact">نمی‌توان ویرایش کرد</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..078769942c584916e10bd21d0488e6c92289aa16 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -0,0 +1,207 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_user_sent_image">%1$s lähetti kuvan.</string> + + <string name="notice_room_invite_no_invitee">Käyttäjän %s kutsu</string> + <string name="notice_room_invite">%1$s kutsui käyttäjän %2$s</string> + <string name="notice_room_invite_you">%1$s kutsui sinut</string> + <string name="notice_room_join">%1$s liittyi huoneeseen</string> + <string name="notice_room_leave">%1$s poistui huoneesta</string> + <string name="notice_room_reject">%1$s hylkäsi kutsun</string> + <string name="notice_room_kick">%1$s poisti käyttäjän %2$s</string> + <string name="notice_room_unban">%1$s poisti porttikiellon käyttäjältä %2$s</string> + <string name="notice_room_ban">%1$s antoi porttikiellon käyttäjälle %2$s</string> + <string name="notice_room_withdraw">%1$s veti takaisin kutsun käyttäjälle %2$s</string> + <string name="notice_avatar_url_changed">%1$s vaihtoi profiilikuvaansa</string> + <string name="notice_display_name_set">%1$s asetti näyttönimekseen %2$s</string> + <string name="notice_display_name_changed_from">%1$s muutti näyttönimensä nimestä %2$s nimeen %3$s</string> + <string name="notice_display_name_removed">%1$s poisti näyttönimensä (%2$s)</string> + <string name="notice_room_topic_changed">%1$s vaihtoi aiheeksi: %2$s</string> + <string name="notice_room_name_changed">%1$s vaihtoi huoneen nimeksi %2$s</string> + <string name="notice_placed_video_call">%s soitti videopuhelun.</string> + <string name="notice_placed_voice_call">%s soitti äänipuhelun.</string> + <string name="notice_answered_call">%s vastasi puheluun.</string> + <string name="notice_ended_call">%s lopetti puhelun.</string> + <string name="notice_made_future_room_visibility">%1$s muutti tulevan huonehistorian näkyväksi seuraaville: %2$s</string> + <string name="notice_room_visibility_invited">kaikki huoneen jäsenet, kutsumisestaan asti.</string> + <string name="notice_room_visibility_joined">kaikki huoneen jäsenet, liittymisestään asti.</string> + <string name="notice_room_visibility_shared">kaikki huoneen jäsenet.</string> + <string name="notice_room_visibility_world_readable">kaikki.</string> + <string name="notice_room_visibility_unknown">tuntematon (%s).</string> + <string name="notice_end_to_end">%1$s otti käyttöön osapuolten välisen salauksen (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s lähetti VoIP-konferenssipyynnön</string> + <string name="notice_voip_started">VoIP-konferenssi alkoi</string> + <string name="notice_voip_finished">VoIP-konferenssi päättyi</string> + + <string name="notice_avatar_changed_too">(myös kuva vaihdettiin)</string> + <string name="notice_room_name_removed">%1$s poisti huoneen nimen</string> + <string name="notice_room_topic_removed">%1$s poisti huoneen aiheen</string> + <string name="notice_profile_change_redacted">%1$s päivitti profiilinsa %2$s</string> + <string name="notice_room_third_party_invite">%1$s lähetti liittymiskutsun huoneeseen käyttäjälle %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s hyväksyi kutsun käyttäjän %2$s puolesta</string> + <string name="notice_crypto_unable_to_decrypt">** Salauksen purku epäonnistui: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin.</string> + + <string name="unable_to_send_message">Viestin lähetys epäonnistui</string> + + <string name="message_failed_to_upload">Kuvan lataaminen epäonnistui</string> + + <string name="network_error">Verkkovirhe</string> + <string name="matrix_error">Matrix-virhe</string> + + <string name="room_error_join_failed_empty_room">Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen.</string> + + <string name="encrypted_message">Salattu viesti</string> + + <string name="medium_email">Sähköpostiosoite</string> + <string name="medium_phone_number">Puhelinnumero</string> + + + <string name="could_not_redact">Takaisinveto epäonnistui</string> + <string name="summary_message">%1$s: %2$s</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Kutsu käyttäjältä %s</string> <!-- Grammar problem --> + <string name="room_displayname_room_invite">Huonekutsu</string> + <string name="room_displayname_two_members">%1$s ja %2$s</string> + <string name="room_displayname_empty_room">Tyhjä huone</string> + + + <string name="summary_user_sent_sticker">%1$s lähetti tarran.</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s ja yksi muu</item> + <item quantity="other">%1$s ja %2$d muuta</item> + </plurals> + + <string name="notice_event_redacted">Viesti poistettu</string> + <string name="notice_event_redacted_by">%1$s poisti viestin</string> + <string name="notice_event_redacted_with_reason">Viesti poistettu [syy: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">%1$s poisti viestin [syy: %2$s]</string> + <string name="verification_emoji_dog">Koira</string> + <string name="verification_emoji_cat">Kissa</string> + <string name="verification_emoji_lion">Leijona</string> + <string name="verification_emoji_horse">Hevonen</string> + <string name="verification_emoji_unicorn">Yksisarvinen</string> + <string name="verification_emoji_pig">Sika</string> + <string name="verification_emoji_elephant">Norsu</string> + <string name="verification_emoji_rabbit">Kani</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kukko</string> + <string name="verification_emoji_penguin">Pingviini</string> + <string name="verification_emoji_turtle">Kilpikonna</string> + <string name="verification_emoji_fish">Kala</string> + <string name="verification_emoji_octopus">Tursas</string> + <string name="verification_emoji_butterfly">Perhonen</string> + <string name="verification_emoji_flower">Kukka</string> + <string name="verification_emoji_tree">Puu</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Sieni</string> + <string name="verification_emoji_globe">Maapallo</string> + <string name="verification_emoji_moon">Kuu</string> + <string name="verification_emoji_cloud">Pilvi</string> + <string name="verification_emoji_fire">Tuli</string> + <string name="verification_emoji_banana">Banaani</string> + <string name="verification_emoji_apple">Omena</string> + <string name="verification_emoji_strawberry">Mansikka</string> + <string name="verification_emoji_corn">Maissi</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Kakku</string> + <string name="verification_emoji_heart">Sydän</string> + <string name="verification_emoji_smiley">Hymiö</string> + <string name="verification_emoji_robot">Robotti</string> + <string name="verification_emoji_hat">Hattu</string> + <string name="verification_emoji_glasses">Silmälasit</string> + <string name="verification_emoji_wrench">Jakoavain</string> + <string name="verification_emoji_santa">Joulupukki</string> + <string name="verification_emoji_thumbsup">Peukut ylös</string> + <string name="verification_emoji_umbrella">Sateenvarjo</string> + <string name="verification_emoji_hourglass">Tiimalasi</string> + <string name="verification_emoji_clock">Kello</string> + <string name="verification_emoji_gift">Lahja</string> + <string name="verification_emoji_lightbulb">Hehkulamppu</string> + <string name="verification_emoji_book">Kirja</string> + <string name="verification_emoji_pencil">Lyijykynä</string> + <string name="verification_emoji_paperclip">Klemmari</string> + <string name="verification_emoji_scissors">Sakset</string> + <string name="verification_emoji_lock">Lukko</string> + <string name="verification_emoji_key">Avain</string> + <string name="verification_emoji_hammer">Vasara</string> + <string name="verification_emoji_telephone">Puhelin</string> + <string name="verification_emoji_flag">Lippu</string> + <string name="verification_emoji_train">Juna</string> + <string name="verification_emoji_bicycle">Polkupyörä</string> + <string name="verification_emoji_airplane">Lentokone</string> + <string name="verification_emoji_rocket">Raketti</string> + <string name="verification_emoji_trophy">Palkinto</string> + <string name="verification_emoji_ball">Pallo</string> + <string name="verification_emoji_guitar">Kitara</string> + <string name="verification_emoji_trumpet">Trumpetti</string> + <string name="verification_emoji_bell">Soittokello</string> + <string name="verification_emoji_anchor">Ankkuri</string> + <string name="verification_emoji_headphone">Kuulokkeet</string> + <string name="verification_emoji_folder">Kansio</string> + <string name="verification_emoji_pin">Nuppineula</string> + + <string name="initial_sync_start_importing_account">Alkusynkronointi: +\nTuodaan tiliä…</string> + <string name="initial_sync_start_importing_account_crypto">Alkusynkronointi: +\nTuodaan kryptoa</string> + <string name="initial_sync_start_importing_account_rooms">Alkusynkronointi: +\nTuodaan huoneita</string> + <string name="initial_sync_start_importing_account_joined_rooms">Alkusynkronointi: +\nTuodaan liityttyjä huoneita</string> + <string name="initial_sync_start_importing_account_invited_rooms">Alkusynkronointi: +\nTuodaan kutsuttuja huoneita</string> + <string name="initial_sync_start_importing_account_left_rooms">Alkusynkronointi: +\nTuodaan poistuttuja huoneita</string> + <string name="initial_sync_start_importing_account_groups">Alkusynkronointi: +\nTuodaan yhteisöjä</string> + <string name="initial_sync_start_importing_account_data">Alkusynkronointi: +\nTuodaan tilin tietoja</string> + + <string name="notice_room_update">%s päivitti tämän huoneen.</string> + + <string name="event_status_sending_message">Lähetetään viestiä…</string> + <string name="clear_timeline_send_queue">Tyhjennä lähetysjono</string> + + <string name="notice_room_third_party_revoked_invite">%1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen</string> + <string name="notice_room_invite_no_invitee_with_reason">Henkilön %1$s kutsu. Syy: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s kutsui henkilön %2$s. Syy: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s kutsui sinut. Syy: %2$s</string> + <string name="notice_room_join_with_reason">%1$s liittyi huoneeseen. Syy: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s poistui huoneesta. Syy: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s hylkäsi kutsun. Syy: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s poisti käyttäjän %2$s huoneesta. Syy: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s poisti porttikiellon käyttäjältä %2$s. Syy: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s antoi porttikiellon käyttäjälle %2$s. Syy: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s lähetti kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s kumosi kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s veti takaisin käyttäjän %2$s kutsun. Syy: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s lisäsi tälle huoneelle osoitteen %2$s.</item> + <item quantity="other">%1$s lisäsi tälle huoneelle osoitteet %2$s.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s poisti tältä huoneelta osoitteen %2$s.</item> + <item quantity="other">%1$s poisti tältä huoneelta osoitteet %3$s.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s.</string> + + <string name="notice_room_canonical_alias_set">%1$s asetti tämän huoneen pääosoitteeksi %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s poisti tämän huoneen pääosoitteen.</string> + + <string name="notice_room_guest_access_can_join">%1$s salli vieraiden liittyä huoneeseen.</string> + <string name="notice_room_guest_access_forbidden">%1$s esti vieraita liittymästä huoneeseen.</string> + + <string name="notice_end_to_end_ok">%1$s laittoi päälle osapuolten välisen salauksen.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s).</string> + + <string name="key_verification_request_fallback_message">%s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..aad3bd1afb6aee9285227eb4ff49acd6dfc8fcb4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -0,0 +1,207 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s : %2$s</string> + <string name="summary_user_sent_image">%1$s a envoyé une image.</string> + + <string name="notice_room_invite_no_invitee">invitation de %s</string> + <string name="notice_room_invite">%1$s a invité %2$s</string> + <string name="notice_room_invite_you">%1$s vous a invité</string> + <string name="notice_room_join">%1$s a rejoint le salon</string> + <string name="notice_room_leave">%1$s est parti du salon</string> + <string name="notice_room_reject">%1$s a rejeté l’invitation</string> + <string name="notice_room_kick">%1$s a expulsé %2$s</string> + <string name="notice_room_unban">%1$s a révoqué le bannissement de %2$s</string> + <string name="notice_room_ban">%1$s a banni %2$s</string> + <string name="notice_room_withdraw">%1$s a annulé l’invitation de %2$s</string> + <string name="notice_avatar_url_changed">%1$s a changé d’avatar</string> + <string name="notice_display_name_set">%1$s a modifié son nom affiché en %2$s</string> + <string name="notice_display_name_changed_from">%1$s a modifié son nom affiché %2$s en %3$s</string> + <string name="notice_display_name_removed">%1$s a supprimé son nom affiché (%2$s)</string> + <string name="notice_room_topic_changed">%1$s a changé le sujet en : %2$s</string> + <string name="notice_room_name_changed">%1$s a changé le nom du salon en : %2$s</string> + <string name="notice_placed_video_call">%s a passé un appel vidéo.</string> + <string name="notice_placed_voice_call">%s a passé un appel vocal.</string> + <string name="notice_answered_call">%s a répondu à l’appel.</string> + <string name="notice_ended_call">%s a raccroché.</string> + <string name="notice_made_future_room_visibility">%1$s a rendu l’historique futur du salon visible pour %2$s</string> + <string name="notice_room_visibility_invited">tous les membres du salon, depuis qu’ils ont été invités.</string> + <string name="notice_room_visibility_joined">tous les membres du salon, depuis qu’ils l’ont rejoint.</string> + <string name="notice_room_visibility_shared">tous les membres du salon.</string> + <string name="notice_room_visibility_world_readable">n’importe qui.</string> + <string name="notice_room_visibility_unknown">inconnu (%s).</string> + <string name="notice_end_to_end">%1$s a activé le chiffrement de bout en bout (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s a demandé une téléconférence VoIP</string> + <string name="notice_voip_started">Téléconférence VoIP démarrée</string> + <string name="notice_voip_finished">Téléconférence VoIP terminée</string> + + <string name="notice_avatar_changed_too">(l’avatar a aussi changé)</string> + <string name="notice_room_name_removed">%1$s a supprimé le nom du salon</string> + <string name="notice_room_topic_removed">%1$s a supprimé le sujet du salon</string> + <string name="notice_profile_change_redacted">%1$s a mis à jour son profil %2$s</string> + <string name="notice_room_third_party_invite">%1$s a envoyé une invitation à %2$s pour rejoindre le salon</string> + <string name="notice_room_third_party_registered_invite">%1$s a accepté l’invitation pour %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Déchiffrement impossible : %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message.</string> + + <string name="could_not_redact">Effacement impossible</string> + <string name="unable_to_send_message">Envoi du message impossible</string> + + <string name="message_failed_to_upload">L’envoi de l’image a échoué</string> + + <string name="network_error">Erreur de réseau</string> + <string name="matrix_error">Erreur de Matrix</string> + + <string name="room_error_join_failed_empty_room">Il est impossible pour le moment de revenir dans un salon vide.</string> + + <string name="encrypted_message">Message chiffré</string> + + <string name="medium_email">Adresse e-mail</string> + <string name="medium_phone_number">Numéro de téléphone</string> + + <string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string> + + <string name="room_displayname_invite_from">Invitation de %s</string> + <string name="room_displayname_room_invite">Invitation au salon</string> + <string name="room_displayname_empty_room">Salon vide</string> + <string name="room_displayname_two_members">%1$s et %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s et 1 autre</item> + <item quantity="other">%1$s et %2$d autres</item> + </plurals> + + + <string name="notice_event_redacted">Message supprimé</string> + <string name="notice_event_redacted_by">Message supprimé par %1$s</string> + <string name="notice_event_redacted_with_reason">Message supprimé [motif : %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Message supprimé par %1$s [motif : %2$s]</string> + <string name="verification_emoji_dog">Chien</string> + <string name="verification_emoji_cat">Chat</string> + <string name="verification_emoji_lion">Lion</string> + <string name="verification_emoji_horse">Cheval</string> + <string name="verification_emoji_unicorn">Licorne</string> + <string name="verification_emoji_pig">Cochon</string> + <string name="verification_emoji_elephant">Éléphant</string> + <string name="verification_emoji_rabbit">Lapin</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Coq</string> + <string name="verification_emoji_penguin">Manchot</string> + <string name="verification_emoji_turtle">Tortue</string> + <string name="verification_emoji_fish">Poisson</string> + <string name="verification_emoji_octopus">Pieuvre</string> + <string name="verification_emoji_butterfly">Papillon</string> + <string name="verification_emoji_flower">Fleur</string> + <string name="verification_emoji_tree">Arbre</string> + <string name="verification_emoji_cactus">Cactus</string> + <string name="verification_emoji_mushroom">Champignon</string> + <string name="verification_emoji_globe">Terre</string> + <string name="verification_emoji_moon">Lune</string> + <string name="verification_emoji_cloud">Nuage</string> + <string name="verification_emoji_fire">Feu</string> + <string name="verification_emoji_banana">Banane</string> + <string name="verification_emoji_apple">Pomme</string> + <string name="verification_emoji_strawberry">Fraise</string> + <string name="verification_emoji_corn">Maïs</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Gâteau</string> + <string name="verification_emoji_heart">CÅ“ur</string> + <string name="verification_emoji_smiley">Smiley</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Chapeau</string> + <string name="verification_emoji_glasses">Lunettes</string> + <string name="verification_emoji_wrench">Clé plate</string> + <string name="verification_emoji_santa">Père Noël</string> + <string name="verification_emoji_thumbsup">Pouce levé</string> + <string name="verification_emoji_umbrella">Parapluie</string> + <string name="verification_emoji_hourglass">Sablier</string> + <string name="verification_emoji_clock">Horloge</string> + <string name="verification_emoji_gift">Cadeau</string> + <string name="verification_emoji_lightbulb">Ampoule</string> + <string name="verification_emoji_book">Livre</string> + <string name="verification_emoji_pencil">Crayon</string> + <string name="verification_emoji_paperclip">Trombone</string> + <string name="verification_emoji_scissors">Ciseaux</string> + <string name="verification_emoji_lock">Cadenas</string> + <string name="verification_emoji_key">Clé</string> + <string name="verification_emoji_hammer">Marteau</string> + <string name="verification_emoji_telephone">Téléphone</string> + <string name="verification_emoji_flag">Drapeau</string> + <string name="verification_emoji_train">Train</string> + <string name="verification_emoji_bicycle">Vélo</string> + <string name="verification_emoji_airplane">Avion</string> + <string name="verification_emoji_rocket">Fusée</string> + <string name="verification_emoji_trophy">Trophée</string> + <string name="verification_emoji_ball">Balle</string> + <string name="verification_emoji_guitar">Guitare</string> + <string name="verification_emoji_trumpet">Trompette</string> + <string name="verification_emoji_bell">Cloche</string> + <string name="verification_emoji_anchor">Ancre</string> + <string name="verification_emoji_headphone">Écouteurs</string> + <string name="verification_emoji_folder">Dossier</string> + <string name="verification_emoji_pin">Épingle</string> + + <string name="initial_sync_start_importing_account">Synchronisation initiale : +\nImportation du compte…</string> + <string name="initial_sync_start_importing_account_crypto">Synchronisation initiale : +\nImportation de la cryptographie</string> + <string name="initial_sync_start_importing_account_rooms">Synchronisation initiale : +\nImportation des salons</string> + <string name="initial_sync_start_importing_account_joined_rooms">Synchronisation initiale : +\nImportation des salons que vous avez rejoints</string> + <string name="initial_sync_start_importing_account_invited_rooms">Synchronisation initiale : +\nImportation des salons où vous avez été invités</string> + <string name="initial_sync_start_importing_account_left_rooms">Synchronisation initiale : +\nImportation des salons que vous avez quittés</string> + <string name="initial_sync_start_importing_account_groups">Synchronisation initiale : +\nImportation des communautés</string> + <string name="initial_sync_start_importing_account_data">Synchronisation initiale : +\nImportation des données du compte</string> + + <string name="notice_room_update">%s a mis à niveau ce salon.</string> + + <string name="event_status_sending_message">Envoi du message…</string> + <string name="clear_timeline_send_queue">Vider la file d’envoi</string> + + <string name="notice_room_third_party_revoked_invite">%1$s a révoqué l’invitation pour %2$s à rejoindre le salon</string> + <string name="notice_room_invite_no_invitee_with_reason">Invitation de %1$s. Raison : %2$s</string> + <string name="notice_room_invite_with_reason">%1$s a invité %2$s. Raison : %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s vous a invité. Raison : %2$s</string> + <string name="notice_room_join_with_reason">%1$s a rejoint le salon. Raison : %2$s</string> + <string name="notice_room_leave_with_reason">%1$s est parti du salon. Raison : %2$s</string> + <string name="notice_room_reject_with_reason">%1$s a refusé l’invitation. Raison : %2$s</string> + <string name="notice_room_kick_with_reason">%1$s a expulsé %2$s. Raison : %3$s</string> + <string name="notice_room_unban_with_reason">%1$s a révoqué le bannissement de %2$s. Raison : %3$s</string> + <string name="notice_room_ban_with_reason">%1$s a banni %2$s. Raison : %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s a accepté l’invitation pour %2$s. Raison : %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s a annulé l’invitation de %2$s. Raison : %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s a ajouté %2$s comme adresse pour ce salon.</item> + <item quantity="other">%1$s a ajouté %2$s comme adresses pour ce salon.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s a supprimé %2$s comme adresse pour ce salon.</item> + <item quantity="other">%1$s a supprimé %3$s comme adresses pour ce salon.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon.</string> + + <string name="notice_room_canonical_alias_set">%1$s a défini %2$s comme adresse principale pour ce salon.</string> + <string name="notice_room_canonical_alias_unset">%1$s a supprimé l’adresse principale de ce salon.</string> + + <string name="notice_room_guest_access_can_join">%1$s a autorisé les visiteurs à rejoindre le salon.</string> + <string name="notice_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string> + + <string name="notice_end_to_end_ok">%1$s a activé le chiffrement de bout en bout.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu).</string> + + <string name="key_verification_request_fallback_message">%s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés.</string> + + <string name="notice_room_created">%1$s a créé le salon</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-gl/strings.xml b/matrix-sdk-android/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..77868e7df3a8f96a6976659cb4b63400aeadb841 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-gl/strings.xml @@ -0,0 +1,68 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="medium_email">Enderezo de correo</string> + <string name="message_failed_to_upload">Fallo ao subir a páxina</string> + + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s enviou unha imaxe.</string> + <string name="summary_user_sent_sticker">%1$s enviou unha icona.</string> + + <string name="notice_room_invite_no_invitee">Convite de %s</string> + <string name="notice_room_invite">%1$s convidou a %2$s</string> + <string name="notice_room_invite_you">%1$s convidouno</string> + <string name="notice_room_join">%1$s entrou</string> + <string name="notice_room_leave">%1$s saÃu</string> + <string name="notice_room_reject">%1$s rexeitou o convite</string> + <string name="notice_room_kick">%1$s expulsou a %2$s</string> + <string name="notice_room_unban">%1$s desbloqueou a %2$s</string> + <string name="notice_room_ban">%1$s bloqueou a %2$s</string> + <string name="notice_room_withdraw">%1$s cancelou o convite de %2$s</string> + <string name="notice_avatar_url_changed">%1$s cambiou o seu avatar</string> + <string name="notice_display_name_set">%1$s cambiou o seu nome a %2$s</string> + <string name="notice_display_name_changed_from">%1$s cambiou o seu nome de %2$s a %3$s</string> + <string name="notice_display_name_removed">%1$s borrou o seu nome público (%2$s)</string> + <string name="notice_room_topic_changed">%1$s cambiou o tema desta sala para: %2$s</string> + <string name="notice_room_name_changed">%1$s cambiou o nome desta sala para: %2$s</string> + <string name="notice_placed_video_call">%s iniciou unha chamada de vÃdeo.</string> + <string name="notice_placed_voice_call">%s iniciou unha chamada de voz.</string> + <string name="notice_answered_call">%s respondeu á chamada.</string> + <string name="notice_ended_call">%s terminou a chamada.</string> + <string name="notice_made_future_room_visibility">%1$s fixo visible os próximos históricos para %2$s</string> + <string name="notice_room_visibility_invited">toda a xente que integran esta sala, desde o momento en que foron convidados.</string> + <string name="notice_room_visibility_joined">todas a xente da sala, desde o momento en que entraron.</string> + <string name="notice_room_visibility_shared">todas os membros da sala.</string> + <string name="notice_room_visibility_world_readable">todos.</string> + <string name="notice_room_visibility_unknown">descoñecido (%s).</string> + <string name="notice_end_to_end">%1$s activou a criptografÃa par-a-par (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s solicitou unha conferencia VoIP</string> + <string name="notice_voip_started">A conferencia VoIP comenzou</string> + <string name="notice_voip_finished">A conferencia VoIP terminou</string> + + <string name="notice_avatar_changed_too">(o avatar tamén foi cambiado)</string> + <string name="notice_room_name_removed">%1$s borrou o nome da sala</string> + <string name="notice_room_topic_removed">%1$s removeu o tema da sala</string> + <string name="notice_profile_change_redacted">%1$s actualizou o seu perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s envioulle un convite a %2$s para que entre na sala</string> + <string name="notice_room_third_party_registered_invite">%1$s aceptou o convite para %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** ImposÃbel descifrar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">O dispositivo do que envÃa non enviou as chaves desta mensaxe.</string> + + <string name="could_not_redact">Non se puido redactar</string> + <string name="unable_to_send_message">Non foi posÃbel enviar a mensaxe</string> + + <string name="network_error">Erro da conexión</string> + <string name="matrix_error">Erro de Matrix</string> + + <string name="room_error_join_failed_empty_room">AÃnda non é posÃbel volver a entrar nunha sala baleira.</string> + + <string name="encrypted_message">Mensaxe cifrada</string> + + <string name="medium_phone_number">Número de teléfono</string> + + <string name="room_displayname_two_members">%1$s e %2$s</string> + + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..35f35eaecd14490e053a40959a189df8221addab --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -0,0 +1,206 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s küldött egy képet.</string> + + <string name="notice_room_invite_no_invitee">%s meghÃvója</string> + <string name="notice_room_invite">%1$s meghÃvta: %2$s</string> + <string name="notice_room_invite_you">%1$s meghÃvott</string> + <string name="notice_room_join">%1$s belépett a szobába</string> + <string name="notice_room_leave">%1$s kilépett a szobából</string> + <string name="notice_room_reject">%1$s elutasÃtotta a meghÃvást</string> + <string name="notice_room_kick">%1$s kidobta: %2$s</string> + <string name="notice_room_unban">%1$s feloldotta %2$s tiltását</string> + <string name="notice_room_ban">%1$s kitiltotta: %2$s</string> + <string name="notice_room_withdraw">%1$s visszavonta %2$s meghÃvását</string> + <string name="notice_avatar_url_changed">%1$s megváltoztatta a profilképét</string> + <string name="notice_display_name_set">%1$s megváltoztatta a megjelenÅ‘ nevét erre: %2$s</string> + <string name="notice_display_name_changed_from">%1$s megváltoztatta a megjelenÃtendÅ‘ nevét errÅ‘l: %2$s, erre: %3$s</string> + <string name="notice_display_name_removed">%1$s eltávolÃtotta a megjelenÃtendÅ‘ nevét (%2$s)</string> + <string name="notice_room_topic_changed">%1$s megváltoztatta a témát erre: %2$s</string> + <string name="notice_room_name_changed">%1$s megváltoztatta a szoba nevét erre: %2$s</string> + <string name="notice_placed_video_call">%s videóhÃvást kezdeményezett.</string> + <string name="notice_placed_voice_call">%s hanghÃvást kezdeményezett.</string> + <string name="notice_answered_call">%s fogadta a hÃvást.</string> + <string name="notice_ended_call">%s befejezte a hÃvást.</string> + <string name="notice_made_future_room_visibility">%1$s láthatóvá tette a jövÅ‘beli elÅ‘zményeket %2$s számára</string> + <string name="notice_room_visibility_invited">az összes szobatag, onnantól, hogy meg lettek hÃvva.</string> + <string name="notice_room_visibility_joined">az összes szobatag, onnantól, hogy csatlakoztak.</string> + <string name="notice_room_visibility_shared">az összes szobatag.</string> + <string name="notice_room_visibility_world_readable">bárki.</string> + <string name="notice_room_visibility_unknown">ismeretlen (%s).</string> + <string name="notice_end_to_end">%1$s bekapcsolta a végpontok közötti titkosÃtást (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s hanghÃvás konferenciát kérelmezett</string> + <string name="notice_voip_started">HanghÃvás konferencia elindult</string> + <string name="notice_voip_finished">HanghÃvás konferencia befejezÅ‘dött</string> + + <string name="notice_avatar_changed_too">(a profilkép is megváltozott)</string> + <string name="notice_room_name_removed">%1$s eltávolÃtotta a szoba nevét</string> + <string name="notice_room_topic_removed">%1$s eltávolÃtotta a szoba témáját</string> + <string name="notice_profile_change_redacted">%1$s megváltoztatta a(z) %2$s profilját</string> + <string name="notice_room_third_party_invite">%1$s meghÃvót küldött %2$s számára, hogy csatlakozzon a szobához</string> + <string name="notice_room_third_party_registered_invite">%1$s elfogadta a meghÃvót ebbe: %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Visszafejtés sikertelen: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">A küldÅ‘ eszköze nem küldte el a kulcsokat ehhez az üzenethez.</string> + + <string name="could_not_redact">Kitakarás sikertelen</string> + <string name="unable_to_send_message">Ãœzenet küldése sikertelen</string> + + <string name="message_failed_to_upload">Kép feltöltése sikertelen</string> + + <string name="network_error">Hálózati hiba</string> + <string name="matrix_error">Matrix hiba</string> + + <string name="room_error_join_failed_empty_room">Jelenleg nem lehetséges újracsatlakozni egy üres szobához.</string> + + <string name="encrypted_message">TitkosÃtott üzenet</string> + + <string name="medium_email">E-mail cÃm</string> + <string name="medium_phone_number">Telefonszám</string> + + <string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string> + + <string name="room_displayname_invite_from">MeghÃvó tÅ‘le: %s</string> + <string name="room_displayname_room_invite">MeghÃvó egy szobába</string> + <string name="room_displayname_two_members">%1$s és %2$s</string> + <string name="room_displayname_empty_room">Ãœres szoba</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s és 1 másik</item> + <item quantity="other">%1$s és %2$d másik</item> + </plurals> + + + <string name="notice_event_redacted">Ãœzenet eltávolÃtva</string> + <string name="notice_event_redacted_by">Ãœzenetet eltávolÃtotta: %1$s</string> + <string name="notice_event_redacted_with_reason">Ãœzenet eltávolÃtva [ok: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Ãœzenetet eltávolÃtotta: %1$s [ok: %2$s]</string> + <string name="verification_emoji_dog">Kutya</string> + <string name="verification_emoji_cat">Macska</string> + <string name="verification_emoji_lion">Oroszlán</string> + <string name="verification_emoji_horse">Ló</string> + <string name="verification_emoji_unicorn">Egyszarvú</string> + <string name="verification_emoji_pig">Malac</string> + <string name="verification_emoji_elephant">Elefánt</string> + <string name="verification_emoji_rabbit">Nyúl</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kakas</string> + <string name="verification_emoji_penguin">Pingvin</string> + <string name="verification_emoji_turtle">TeknÅ‘s</string> + <string name="verification_emoji_fish">Hal</string> + <string name="verification_emoji_octopus">Polip</string> + <string name="verification_emoji_butterfly">Pillangó</string> + <string name="verification_emoji_flower">Virág</string> + <string name="verification_emoji_tree">Fa</string> + <string name="verification_emoji_cactus">Kaktusz</string> + <string name="verification_emoji_mushroom">Gomba</string> + <string name="verification_emoji_globe">Föld</string> + <string name="verification_emoji_moon">Hold</string> + <string name="verification_emoji_cloud">FelhÅ‘</string> + <string name="verification_emoji_fire">Tűz</string> + <string name="verification_emoji_banana">Banán</string> + <string name="verification_emoji_apple">Alma</string> + <string name="verification_emoji_strawberry">Eper</string> + <string name="verification_emoji_corn">Kukorica</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Süti</string> + <string name="verification_emoji_heart">SzÃv</string> + <string name="verification_emoji_smiley">Smiley</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Kalap</string> + <string name="verification_emoji_glasses">Szemüveg</string> + <string name="verification_emoji_wrench">Csavarkulcs</string> + <string name="verification_emoji_santa">Télapó</string> + <string name="verification_emoji_thumbsup">Hüvelykujj fel</string> + <string name="verification_emoji_umbrella">EsernyÅ‘</string> + <string name="verification_emoji_hourglass">Homokóra</string> + <string name="verification_emoji_clock">Óra</string> + <string name="verification_emoji_gift">Ajándék</string> + <string name="verification_emoji_lightbulb">ÉgÅ‘</string> + <string name="verification_emoji_book">Könyv</string> + <string name="verification_emoji_pencil">Ceruza</string> + <string name="verification_emoji_paperclip">Gémkapocs</string> + <string name="verification_emoji_scissors">Olló</string> + <string name="verification_emoji_lock">Zár</string> + <string name="verification_emoji_key">Kulcs</string> + <string name="verification_emoji_hammer">Kalapács</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Zászló</string> + <string name="verification_emoji_train">Vonat</string> + <string name="verification_emoji_bicycle">Kerékpár</string> + <string name="verification_emoji_airplane">RepülÅ‘</string> + <string name="verification_emoji_rocket">Rakéta</string> + <string name="verification_emoji_trophy">Trófea</string> + <string name="verification_emoji_ball">Labda</string> + <string name="verification_emoji_guitar">Gitár</string> + <string name="verification_emoji_trumpet">Trombita</string> + <string name="verification_emoji_bell">Harang</string> + <string name="verification_emoji_anchor">Vasmacska</string> + <string name="verification_emoji_headphone">Fejhallgató</string> + <string name="verification_emoji_folder">Mappa</string> + <string name="verification_emoji_pin">Tű</string> + + <string name="initial_sync_start_importing_account">Induló szinkronizáció: +\nFiók betöltése…</string> + <string name="initial_sync_start_importing_account_crypto">Induló szinkronizáció: +\nTitkosÃtás betöltése</string> + <string name="initial_sync_start_importing_account_rooms">Induló szinkronizáció: +\nSzobák betöltése</string> + <string name="initial_sync_start_importing_account_joined_rooms">Induló szinkronizáció: +\nCsatlakozott szobák betöltése</string> + <string name="initial_sync_start_importing_account_invited_rooms">Induló szinkronizáció: +\nMeghÃvott szobák betöltése</string> + <string name="initial_sync_start_importing_account_left_rooms">Induló szinkronizáció: +\nElhagyott szobák betöltése</string> + <string name="initial_sync_start_importing_account_groups">Induló szinkronizáció: +\nKözösségek betöltése</string> + <string name="initial_sync_start_importing_account_data">Induló szinkronizáció: +\nFiók adatok betöltése</string> + + <string name="notice_room_update">%s frissÃtette ezt a szobát.</string> + + <string name="event_status_sending_message">Ãœzenet küldése…</string> + <string name="clear_timeline_send_queue">KüldÅ‘ sor ürÃtése</string> + + <string name="notice_room_third_party_revoked_invite">%1$s visszavonta %2$s meghÃvását, hogy csatlakozzon a szobához</string> + <string name="notice_room_invite_no_invitee_with_reason">%1$s meghÃvója. Ok: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s meghÃvta Å‘t: %2$s. Ok: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s meghÃvott. Ok: %2$s</string> + <string name="notice_room_join_with_reason">%1$s belépett a szobába. Mert: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s kilépett a szobából. Ok: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s visszautasÃtotta a meghÃvót. Ok: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s kirúgta Å‘t: %2$s. Ok: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s visszaengedte Å‘t: %2$s. Ok: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s kitiltotta Å‘t: %2$s. Ok: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s meghÃvót küldött neki: %2$s, hogy lépjen be a szobába. Ok: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s visszavonta %2$s meghÃvóját a szobába való belépéshez. Ok: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s elfogadta a meghÃvót ide: %2$s. Ok: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s visszavonta %2$s meghÃvóját. Ok: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s ezt a cÃmet adta a szobához: %2$s.</item> + <item quantity="other">%1$s ezeket a cÃmeket adta a szobához: %2$s.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s ezt a cÃmet törölte a szobából: %3$s.</item> + <item quantity="other">%1$s ezeket a cÃmeket törölte a szobából: %3$s.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s a szobához adta ezeket:%2$s és törölte ezeket: %3$s.</string> + + <string name="notice_room_canonical_alias_set">%1$s a szoba elsÅ‘dleges cÃmét erre állÃtotta be: %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s eltávolÃtotta a szoba elsÅ‘dleges cÃmét.</string> + + <string name="notice_room_guest_access_can_join">%1$s megengedte a vendégeknek, hogy belépjenek ebbe a szobába.</string> + <string name="notice_room_guest_access_forbidden">%1$s megtiltotta a vendégeknek, hogy belépjenek ebbe a szobába.</string> + + <string name="notice_end_to_end_ok">%1$s bekapcsolta a végpontok közötti titkosÃtást.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s bekapcsolta a végpontok közötti titkosÃtást (ismeretlen algoritmus %2$s).</string> + + <string name="key_verification_request_fallback_message">%s kéri a kulcsok ellenÅ‘rzését de a kliens nem támogatja a szobán belüli kulcs ellenÅ‘rzést. A hagyományos módon kell ellenÅ‘rizned a kulcsokat.</string> + + <string name="notice_room_created">%1$s szobát készÃtett</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-id/strings.xml b/matrix-sdk-android/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..157b23d4017375bb9e9a253e19884046623e20fe --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-id/strings.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="room_displayname_invite_from">Undang dari %s</string> + <string name="room_displayname_room_invite">Undangan Ruang</string> + <string name="room_displayname_two_members">%1$s dan %2$s</string> + + <string name="room_displayname_empty_room">Ruang kosong</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$s dan %2$d yang lain</item> + </plurals> +</resources> \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-in/strings.xml b/matrix-sdk-android/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..157b23d4017375bb9e9a253e19884046623e20fe --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-in/strings.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="room_displayname_invite_from">Undang dari %s</string> + <string name="room_displayname_room_invite">Undangan Ruang</string> + <string name="room_displayname_two_members">%1$s dan %2$s</string> + + <string name="room_displayname_empty_room">Ruang kosong</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$s dan %2$d yang lain</item> + </plurals> +</resources> \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-is/strings.xml b/matrix-sdk-android/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecf19edb8ac48e58d210944b4618108df363de25 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-is/strings.xml @@ -0,0 +1,76 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s sendi mynd.</string> + <string name="summary_user_sent_sticker">%1$s sendi lÃmmerki.</string> + + <string name="notice_room_invite_no_invitee">%s sendi boð um þátttöku</string> + <string name="notice_room_invite">%1$s bauð %2$s</string> + <string name="notice_room_invite_you">%1$s bauð þér</string> + <string name="notice_room_join">%1$s gekk à hópinn</string> + <string name="notice_room_leave">%1$s hætti</string> + <string name="notice_room_reject">%1$s hafnaði boðinu</string> + <string name="notice_room_kick">%1$s sparkaði %2$s</string> + <string name="notice_room_unban">%1$s afbannaði %2$s</string> + <string name="notice_room_ban">%1$s bannaði %2$s</string> + <string name="notice_avatar_url_changed">%1$s breyttu auðkennismynd sinni</string> + <string name="notice_room_visibility_invited">allir meðlimir spjallrásar, sÃðan þeim var boðið.</string> + <string name="notice_room_visibility_joined">allir meðlimir spjallrásar, sÃðan þeir skráðu sig.</string> + <string name="notice_room_visibility_shared">allir meðlimir spjallrásar.</string> + <string name="notice_room_visibility_world_readable">hver sem er.</string> + <string name="notice_room_visibility_unknown">óþekktur (%s).</string> + <string name="notice_voip_started">VoIP-sÃmafundur hafinn</string> + <string name="notice_voip_finished">VoIP-sÃmafundi lokið</string> + + <string name="notice_avatar_changed_too">(einnig var skipt um auðkennismynd)</string> + <string name="notice_crypto_unable_to_decrypt">** Mistókst að afkóða: %s **</string> + + <string name="unable_to_send_message">Gat ekki sent skilaboð</string> + + <string name="message_failed_to_upload">Gat ekki sent inn mynd</string> + + <string name="network_error">Villa à netkerfi</string> + <string name="matrix_error">Villa à Matrix</string> + + <string name="encrypted_message">Dulrituð skilaboð</string> + + <string name="medium_email">Tölvupóstfang</string> + <string name="medium_phone_number">SÃmanúmer</string> + + <string name="notice_room_withdraw">%1$s tók til baka boð frá %2$s</string> + <string name="notice_display_name_set">%1$s setti birtingarnafn sitt sem %2$s</string> + <string name="notice_display_name_changed_from">%1$s breytti birtingarnafni sÃnu úr %2$s à %3$s</string> + <string name="notice_display_name_removed">%1$s fjarlægði birtingarnafn sitt (%2$s)</string> + <string name="notice_room_topic_changed">%1$s breytti umræðuefninu Ã: %2$s</string> + <string name="notice_room_name_changed">%1$s breytti heiti spjallrásarinnar Ã: %2$s</string> + <string name="notice_placed_video_call">%s hringdi myndsamtal.</string> + <string name="notice_placed_voice_call">%s hringdi raddsamtal.</string> + <string name="notice_answered_call">%s svaraði sÃmtalinu.</string> + <string name="notice_ended_call">%s lauk sÃmtalinu.</string> + <string name="notice_end_to_end">%1$s kveikti á enda-Ã-enda dulritun (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s bað um VoIP-sÃmafund</string> + <string name="notice_room_name_removed">%1$s fjarlægði heiti spjallrásar</string> + <string name="notice_room_topic_removed">%1$s fjarlægði umfjöllunarefni spjallrásar</string> + <string name="notice_made_future_room_visibility">%1$s gerði ferilskrá spjallrásar héðan à frá sýnilega fyrir %2$s</string> + <string name="notice_profile_change_redacted">%1$s uppfærði notandasniðið sitt %2$s</string> + <string name="notice_room_third_party_invite">%1$s sendi boð til %2$s um þátttöku à spjallrásinni</string> + <string name="notice_room_third_party_registered_invite">%1$s samþykkti boð um að taka þátt à %2$s</string> + + <string name="notice_crypto_error_unkwown_inbound_session_id">Tæki sendandans hefur ekki sent okkur dulritunarlyklana fyrir þessi skilaboð.</string> + + <string name="could_not_redact">Gat ekki ritstýrt</string> + <string name="room_error_join_failed_empty_room">Ekki er à augnablikinu hægt að taka aftur þátt à spjallrás sem er tóm.</string> + + <string name="room_displayname_room_invite">Boð á spjallrás</string> + <string name="room_displayname_two_members">%1$s og %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s og 1 annar</item> + <item quantity="other">%1$s og %2$d aðrir</item> + </plurals> + + <string name="room_displayname_empty_room">Tóm spjallrás</string> + <string name="room_displayname_invite_from">Boð frá %s</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..2b2a097f1323717ae3df0793ea652482b2fdb812 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -0,0 +1,303 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s ha inviato un\'immagine.</string> + + <string name="notice_room_invite_no_invitee">Invito di %s</string> + <string name="notice_room_invite">%1$s ha invitato %2$s</string> + <string name="notice_room_invite_you">%1$s ti ha invitato</string> + <string name="notice_room_join">%1$s è entrato nella stanza</string> + <string name="notice_room_leave">%1$s è uscito dalla stanza</string> + <string name="notice_room_reject">%1$s ha rifiutato l\'invito</string> + <string name="notice_room_kick">%1$s ha buttato fuori %2$s</string> + <string name="notice_room_unban">%1$s ha tolto il bando a %2$s</string> + <string name="notice_room_ban">%1$s ha bandito %2$s</string> + <string name="notice_room_withdraw">%1$s ha revocato l\'invito per %2$s</string> + <string name="notice_avatar_url_changed">%1$s ha modificato il suo avatar</string> + <string name="notice_display_name_set">%1$s hanno cambiato il nome visualizzato con %2$s</string> + <string name="notice_display_name_changed_from">%1$s ha cambiato il nome visualizzato da %2$s a %3$s</string> + <string name="notice_display_name_removed">%1$s ha rimosso il nome visibile (%2$s)</string> + <string name="notice_room_topic_changed">%1$s ha cambiato l\'argomento con: %2$s</string> + <string name="notice_room_name_changed">%1$s ha cambiato il nome della stanza con: %2$s</string> + <string name="notice_placed_video_call">%s ha iniziato una chiamata video.</string> + <string name="notice_placed_voice_call">%s ha iniziato una chiamata vocale.</string> + <string name="notice_answered_call">%s ha risposto alla chiamata.</string> + <string name="notice_ended_call">%s ha terminato la chiamata.</string> + <string name="notice_made_future_room_visibility">%1$s ha reso la futura cronologia della stanza visibile a %2$s</string> + <string name="notice_room_visibility_invited">tutti i membri della stanza, dal momento del loro invito.</string> + <string name="notice_room_visibility_joined">tutti i membri della stanza, dal momento in cui sono entrati.</string> + <string name="notice_room_visibility_shared">tutti i membri della stanza.</string> + <string name="notice_room_visibility_world_readable">chiunque.</string> + <string name="notice_room_visibility_unknown">sconosciuto (%s).</string> + <string name="notice_end_to_end">%1$s ha attivato la crittografia end-to-end (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s ha richiesto una conferenza VoIP</string> + <string name="notice_voip_started">Conferenza VoIP iniziata</string> + <string name="notice_voip_finished">Conferenza VoIP terminata</string> + + <string name="notice_avatar_changed_too">(anche l\'avatar è cambiato)</string> + <string name="notice_room_name_removed">%1$s ha rimosso il nome della stanza</string> + <string name="notice_room_topic_removed">%1$s ha rimosso l\'argomento della stanza</string> + <string name="notice_profile_change_redacted">%1$s ha aggiornato il profilo %2$s</string> + <string name="notice_room_third_party_invite">%1$s ha mandato un invito a %2$s per unirsi alla stanza</string> + <string name="notice_room_third_party_registered_invite">%1$s ha accettato l\'invito per %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Impossibile decriptare: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Il dispositivo del mittente non ci ha inviato le chiavi per questo messaggio.</string> + + <string name="could_not_redact">Impossibile revisionare</string> + <string name="unable_to_send_message">Impossibile inviare il messaggio</string> + + <string name="message_failed_to_upload">Invio dell\'immagine fallito</string> + + <string name="network_error">Errore di rete</string> + <string name="matrix_error">Errore di Matrix</string> + + <string name="room_error_join_failed_empty_room">Al momento non è possibile rientrare in una stanza vuota.</string> + + <string name="encrypted_message">Messaggio criptato</string> + + <string name="medium_email">Indirizzo email</string> + <string name="medium_phone_number">Numero di telefono</string> + + <string name="summary_user_sent_sticker">%1$s ha inviato un adesivo.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Invito da %s</string> + <string name="room_displayname_room_invite">Invito nella stanza</string> + <string name="room_displayname_two_members">%1$s e %2$s</string> + <string name="room_displayname_empty_room">Stanza vuota</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s e 1 altro</item> + <item quantity="other">%1$s e %2$d altri</item> + </plurals> + + + <string name="notice_event_redacted">Messaggio rimosso</string> + <string name="notice_event_redacted_by">Messaggio rimosso da %1$s</string> + <string name="notice_event_redacted_with_reason">Messaggio rimosso [motivo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Messaggio rimosso da %1$s [motivo: %2$s]</string> + <string name="verification_emoji_dog">Cane</string> + <string name="verification_emoji_cat">Gatto</string> + <string name="verification_emoji_lion">Leone</string> + <string name="verification_emoji_horse">Cavallo</string> + <string name="verification_emoji_unicorn">Unicorno</string> + <string name="verification_emoji_pig">Maiale</string> + <string name="verification_emoji_elephant">Elefante</string> + <string name="verification_emoji_rabbit">Coniglio</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Gallo</string> + <string name="verification_emoji_penguin">Pinguino</string> + <string name="verification_emoji_turtle">Tartaruga</string> + <string name="verification_emoji_fish">Pesce</string> + <string name="verification_emoji_octopus">Piovra</string> + <string name="verification_emoji_butterfly">Farfalla</string> + <string name="verification_emoji_flower">Fiore</string> + <string name="verification_emoji_tree">Albero</string> + <string name="verification_emoji_cactus">Cactus</string> + <string name="verification_emoji_mushroom">Fungo</string> + <string name="verification_emoji_globe">Globo</string> + <string name="verification_emoji_moon">Luna</string> + <string name="verification_emoji_cloud">Nuvola</string> + <string name="verification_emoji_fire">Fuoco</string> + <string name="verification_emoji_banana">Banana</string> + <string name="verification_emoji_apple">Mela</string> + <string name="verification_emoji_strawberry">Fragola</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Torta</string> + <string name="verification_emoji_heart">Cuore</string> + <string name="verification_emoji_smiley">Sorriso</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Cappello</string> + <string name="verification_emoji_glasses">Occhiali</string> + <string name="verification_emoji_wrench">Chiave inglese</string> + <string name="verification_emoji_santa">Babbo Natale</string> + <string name="verification_emoji_thumbsup">Pollice in su</string> + <string name="verification_emoji_umbrella">Ombrello</string> + <string name="verification_emoji_hourglass">Clessidra</string> + <string name="verification_emoji_clock">Orologio</string> + <string name="verification_emoji_gift">Regalo</string> + <string name="verification_emoji_lightbulb">Lampadina</string> + <string name="verification_emoji_book">Libro</string> + <string name="verification_emoji_pencil">Matita</string> + <string name="verification_emoji_paperclip">Graffetta</string> + <string name="verification_emoji_scissors">Forbici</string> + <string name="verification_emoji_lock">Lucchetto</string> + <string name="verification_emoji_key">Chiave</string> + <string name="verification_emoji_hammer">Martello</string> + <string name="verification_emoji_telephone">Telefono</string> + <string name="verification_emoji_flag">Bandiera</string> + <string name="verification_emoji_train">Treno</string> + <string name="verification_emoji_bicycle">Bicicletta</string> + <string name="verification_emoji_airplane">Aeroplano</string> + <string name="verification_emoji_rocket">Razzo</string> + <string name="verification_emoji_trophy">Trofeo</string> + <string name="verification_emoji_ball">Palla</string> + <string name="verification_emoji_guitar">Chitarra</string> + <string name="verification_emoji_trumpet">Tromba</string> + <string name="verification_emoji_bell">Campana</string> + <string name="verification_emoji_anchor">Ancora</string> + <string name="verification_emoji_headphone">Cuffie</string> + <string name="verification_emoji_folder">Cartella</string> + <string name="verification_emoji_pin">Spillo</string> + + <string name="initial_sync_start_importing_account">Sync iniziale: +\nImportazione account…</string> + <string name="initial_sync_start_importing_account_crypto">Sync iniziale: +\nImportazione cifratura</string> + <string name="initial_sync_start_importing_account_rooms">Sync iniziale: +\nImportazione stanze</string> + <string name="initial_sync_start_importing_account_joined_rooms">Sync iniziale: +\nImportazione stanze partecipate</string> + <string name="initial_sync_start_importing_account_invited_rooms">Sync iniziale: +\nImportazione stanze invitate</string> + <string name="initial_sync_start_importing_account_left_rooms">Sync iniziale: +\nImportazione stanze lasciate</string> + <string name="initial_sync_start_importing_account_groups">Sync iniziale: +\nImportazione comunità </string> + <string name="initial_sync_start_importing_account_data">Sync iniziale: +\nImportazione dati account</string> + + <string name="notice_room_update">%s ha aggiornato questa stanza.</string> + + <string name="event_status_sending_message">Invio messaggio in corso …</string> + <string name="clear_timeline_send_queue">Cancella la coda di invio</string> + + <string name="notice_room_third_party_revoked_invite">%1$s ha revocato l\'invito a %2$s di unirsi alla stanza</string> + <string name="notice_room_invite_no_invitee_with_reason">Invito di %1$s. Motivo: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s ha invitato %2$s. Motivo: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s ti ha invitato. Motivo: %2$s</string> + <string name="notice_room_join_with_reason">%1$s è entrato nella stanza. Motivo: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s è uscito dalla stanza. Motivo: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s ha rifiutato l\'invito. Motivo: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s ha buttato fuori %2$s. Motivo: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s ha riammesso %2$s. Motivo: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s ha bandito %2$s. Motivo: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s ha inviato un invito a %2$s di unirsi alla stanza. Motivo: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s ha revocato l\'invito a %2$s di unirsi alla stanza. Motivo: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s ha accettato l\'invito per %2$s. Motivo: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s ha rifiutato l\'invito di %2$s. Motivo: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s ha aggiunto %2$s come indirizzo per questa stanza.</item> + <item quantity="other">%1$s ha aggiunto %2$s come indirizzi per questa stanza.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s ha rimosso %2$s come indirizzo per questa stanza.</item> + <item quantity="other">%1$s ha rimosso %3$s come indirizzi per questa stanza.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s ha aggiunto %2$s e rimosso %3$s come indirizzi per questa stanza.</string> + + <string name="notice_room_canonical_alias_set">%1$s ha impostato l\'indirizzo principale per questa stanza a %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s ha rimosso l\'indirizzo principale per questa stanza.</string> + + <string name="notice_room_guest_access_can_join">%1$s ha permesso l\'accesso alla stanza per gli ospiti.</string> + <string name="notice_room_guest_access_forbidden">%1$s ha impedito l\'accesso alla stanza per gli ospiti.</string> + + <string name="notice_end_to_end_ok">%1$s ha attivato la cifratura end-to-end.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s ha attivato la cifratura end-to-end (algoritmo %2$s non riconosciuto).</string> + + <string name="key_verification_request_fallback_message">%s sta chiedendo di verificare la tua chiave, ma il tuo client non supporta la verifica in-chat. Dovrai usare il metodo di verifica obsoleto per verificare le chiavi.</string> + + <string name="notice_room_created">%1$s ha creato la stanza</string> + <string name="summary_you_sent_image">Hai inviato un\'immagine.</string> + <string name="summary_you_sent_sticker">Hai inviato un adesivo.</string> + + <string name="notice_room_invite_no_invitee_by_you">Il tuo invito</string> + <string name="notice_room_created_by_you">Hai creato la stanza</string> + <string name="notice_room_invite_by_you">Hai invitato %1$s</string> + <string name="notice_room_join_by_you">Sei entrato nella stanza</string> + <string name="notice_room_leave_by_you">Sei uscito dalla stanza</string> + <string name="notice_room_reject_by_you">Hai rifiutato l\'invito</string> + <string name="notice_room_kick_by_you">Hai buttato fuori %1$s</string> + <string name="notice_room_unban_by_you">Hai riammesso %1$s</string> + <string name="notice_room_ban_by_you">Hai bandito %1$s</string> + <string name="notice_room_withdraw_by_you">Hai ritirato l\'invito di %1$s</string> + <string name="notice_avatar_url_changed_by_you">Hai cambiato il tuo avatar</string> + <string name="notice_display_name_set_by_you">Hai impostato il tuo nome visualizzato a %1$s</string> + <string name="notice_display_name_changed_from_by_you">Hai cambiato il tuo nome visualizzato da %1$s a %2$s</string> + <string name="notice_display_name_removed_by_you">Hai rimosso il tuo nome visibile (era %1$s)</string> + <string name="notice_room_topic_changed_by_you">Hai cambiato l\'argomento a: %1$s</string> + <string name="notice_room_avatar_changed">%1$s ha modificato l\'avatar della stanza</string> + <string name="notice_room_avatar_changed_by_you">Hai modificato l\'avatar della stanza</string> + <string name="notice_room_name_changed_by_you">Hai cambiato il nome della stanza a: %1$s</string> + <string name="notice_placed_video_call_by_you">Hai iniziato una videochiamata.</string> + <string name="notice_placed_voice_call_by_you">Hai iniziato una telefonata.</string> + <string name="notice_call_candidates">%s ha inviato dati per impostare la chiamata.</string> + <string name="notice_call_candidates_by_you">Hai inviato dati per impostare la chiamata.</string> + <string name="notice_answered_call_by_you">Hai risposto alla chiamata.</string> + <string name="notice_ended_call_by_you">Hai terminato la chiamata.</string> + <string name="notice_made_future_room_visibility_by_you">Hai reso visibile la futura cronologia della stanza a %1$s</string> + <string name="notice_end_to_end_by_you">Hai attivato la crittografia end-to-end (%1$s)</string> + <string name="notice_room_update_by_you">Hai aggiornato questa stanza.</string> + + <string name="notice_requested_voip_conference_by_you">Hai richiesto una conferenza VoIP</string> + <string name="notice_room_name_removed_by_you">Hai rimosso il nome della stanza</string> + <string name="notice_room_topic_removed_by_you">Hai rimosso l\'argomento della stanza</string> + <string name="notice_room_avatar_removed">%1$s ha rimosso l\'avatar della stanza</string> + <string name="notice_room_avatar_removed_by_you">Hai rimosso l\'avatar della stanza</string> + <string name="notice_profile_change_redacted_by_you">Hai aggiornato il tuo profilo %1$s</string> + <string name="notice_room_third_party_invite_by_you">Hai mandato un invito a %1$s a unirsi alla stanza</string> + <string name="notice_room_third_party_revoked_invite_by_you">Hai revocato l\'invito per %1$s a unirsi alla stanza</string> + <string name="notice_room_third_party_registered_invite_by_you">Hai accettato l\'invito per %1$s</string> + + <string name="notice_widget_added">%1$s ha aggiunto il widget %2$s</string> + <string name="notice_widget_added_by_you">Hai aggiunto il widget %1$s</string> + <string name="notice_widget_removed">%1$s ha rimosso il widget %2$s</string> + <string name="notice_widget_removed_by_you">Hai rimosso il widget %1$s</string> + <string name="notice_widget_modified">%1$s ha modificato il widget %2$s</string> + <string name="notice_widget_modified_by_you">Hai modificato il widget %1$s</string> + + <string name="power_level_admin">Amministratore</string> + <string name="power_level_moderator">Moderatore</string> + <string name="power_level_default">Predefinito</string> + <string name="power_level_custom">Personalizzato (%1$d)</string> + <string name="power_level_custom_no_value">Personalizzato</string> + + <string name="notice_power_level_changed_by_you">Hai cambiato il livello di potere di %1$s.</string> + <string name="notice_power_level_changed">%1$s ha cambiato il livello di potere di %2$s.</string> + <string name="notice_power_level_diff">%1$s da %2$s a %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">Il tuo invito. Motivo: %1$s</string> + <string name="notice_room_invite_with_reason_by_you">Hai invitato %1$s. Motivo: %2$s</string> + <string name="notice_room_join_with_reason_by_you">Sei entrato nella stanza. Motivo: %1$s</string> + <string name="notice_room_leave_with_reason_by_you">Sei uscito dalla stanza. Motivo: %1$s</string> + <string name="notice_room_reject_with_reason_by_you">Hai rifiutato l\'invito. Motivo: %1$s</string> + <string name="notice_room_kick_with_reason_by_you">Hai buttato fuori %1$s. Motivo: %2$s</string> + <string name="notice_room_unban_with_reason_by_you">Hai riammesso %1$s. Motivo: %2$s</string> + <string name="notice_room_ban_with_reason_by_you">Hai bandito %1$s. Motivo: %2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Hai mandato un invito a %1$s a unirsi alla stanza. Motivo: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Hai revocato l\'invito a %1$s a unirsi alla stanza. Motivo: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Hai accettato l\'invito per %1$s. Motivo: %2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">Hai ritirato l\'invito di %2$s. Motivo: %2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">Hai aggiunto %1$s come indirizzo per questa stanza.</item> + <item quantity="other">Hai aggiunto %1$s come indirizzi per questa stanza.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">Hai rimosso %1$s come indirizzo per questa stanza.</item> + <item quantity="other">Hai rimosso %2$s come indirizzi per questa stanza.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza.</string> + + <string name="notice_room_canonical_alias_set_by_you">Hai impostato l\'indirizzo principale per questa stanza a %1$s.</string> + <string name="notice_room_canonical_alias_unset_by_you">Hai rimosso l\'indirizzo principale per questa stanza.</string> + + <string name="notice_room_guest_access_can_join_by_you">Hai permesso l\'accesso alla stanza per gli ospiti.</string> + <string name="notice_room_guest_access_forbidden_by_you">Hai impedito l\'accesso alla stanza per gli ospiti.</string> + + <string name="notice_end_to_end_ok_by_you">Hai attivato la crittografia end-to-end.</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Hai attivato la crittografia end-to-end (algoritmo %1$s sconosciuto).</string> + + <string name="call_notification_answer">Accetta</string> + <string name="call_notification_reject">Rifiuta</string> + <string name="call_notification_hangup">Riaggancia</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-ja/strings.xml b/matrix-sdk-android/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..366c743494b7286013e0f471d3721b6ee52858bb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ja/strings.xml @@ -0,0 +1,74 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$sãŒç”»åƒã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚</string> + <string name="summary_user_sent_sticker">%1$sãŒã‚¹ã‚¿ãƒ³ãƒ—ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚</string> + + <string name="notice_room_invite_no_invitee">%sã®æ‹›å¾…</string> + <string name="notice_room_invite">%1$sãŒ%2$sを招待ã—ã¾ã—ãŸ</string> + <string name="notice_room_invite_you">%1$sãŒã‚ãªãŸã‚’招待ã—ã¾ã—ãŸ</string> + <string name="notice_room_join">%1$sãŒå‚åŠ ã—ã¾ã—ãŸ</string> + <string name="notice_room_leave">%1$sãŒé€€å‡ºã—ã¾ã—ãŸ</string> + <string name="notice_room_reject">%1$sãŒæ‹›å¾…ã‚’æ–ã‚Šã¾ã—ãŸ</string> + <string name="notice_room_kick">%1$sãŒ%2$sを追放ã—ã¾ã—ãŸ</string> + <string name="notice_room_unban">%1$sãŒ%2$sをブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã—ãŸ</string> + <string name="notice_room_ban">%1$sãŒ%2$sをブãƒãƒƒã‚¯ã—ã¾ã—ãŸ</string> + <string name="notice_room_withdraw">%1$sãŒ%2$sã®æ‹›å¾…を撤回ã—ã¾ã—ãŸ</string> + <string name="notice_avatar_url_changed">%1$sãŒã‚¢ãƒã‚¿ãƒ¼ã‚’変更ã—ã¾ã—ãŸ</string> + <string name="notice_display_name_set">%1$sãŒè¡¨ç¤ºåã‚’%2$sã«è¨å®šã—ã¾ã—ãŸ</string> + <string name="notice_display_name_changed_from">%1$sãŒè¡¨ç¤ºåã‚’%2$sã‹ã‚‰%3$sã«å¤‰æ›´ã—ã¾ã—ãŸ</string> + <string name="notice_display_name_removed">%1$sãŒè¡¨ç¤ºå (%2$s) を削除ã—ã¾ã—ãŸ</string> + <string name="notice_room_topic_changed">%1$sãŒãƒ†ãƒ¼ãƒžã‚’%2$sã«å¤‰æ›´ã—ã¾ã—ãŸ</string> + <string name="notice_room_name_changed">%1$sãŒéƒ¨å±‹åã‚’%2$sã«å¤‰æ›´ã—ã¾ã—ãŸ</string> + <string name="notice_placed_video_call">%sãŒãƒ“デオ通話を開始ã—ã¾ã—ãŸã€‚</string> + <string name="notice_placed_voice_call">%sãŒéŸ³å£°é€šè©±ã‚’開始ã—ã¾ã—ãŸã€‚</string> + <string name="notice_answered_call">%sãŒé›»è©±ã«å‡ºã¾ã—ãŸã€‚</string> + <string name="notice_ended_call">%sãŒé€šè©±ã‚’終了ã—ã¾ã—ãŸã€‚</string> + <string name="room_displayname_invite_from">%sã•ã‚“ã‹ã‚‰ã®æ‹›å¾…</string> + <string name="room_displayname_room_invite">部屋ã¸ã®æ‹›å¾…</string> + <string name="room_displayname_two_members">%1$sã¨%2$s</string> + <string name="room_displayname_empty_room">空ã®éƒ¨å±‹</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$sã¨ä»–%2$då</item> + </plurals> + + <string name="notice_made_future_room_visibility">%1$sã¯ã€ä»Šå¾Œã®éƒ¨å±‹å±¥æ´ã‚’%2$sã«è¡¨ç¤ºã•ã›ã¾ã—ãŸ</string> + <string name="notice_room_visibility_invited">部屋ã®ãƒ¡ãƒ³ãƒãƒ¼å…¨å“¡ã€æ‹›å¾…ã•ã‚ŒãŸæ™‚点ã‹ã‚‰ã€‚</string> + <string name="notice_room_visibility_joined">部屋ã®ãƒ¡ãƒ³ãƒãƒ¼å…¨å“¡ã€å‚åŠ ã—ãŸæ™‚点ã‹ã‚‰ã€‚</string> + <string name="notice_room_visibility_shared">部屋ã®ãƒ¡ãƒ³ãƒãƒ¼å…¨å“¡ã€‚</string> + <string name="notice_room_visibility_world_readable">誰ã§ã‚‚。</string> + <string name="notice_room_visibility_unknown">ä¸æ˜Ž (%s)。</string> + <string name="notice_end_to_end">%1$s ãŒã‚¨ãƒ³ãƒ‰ãƒ„ーエンド暗å·åŒ–を有効ã«ã—ã¾ã—㟠(%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s ãŒVoIP会è°ã‚’リクエストã—ã¾ã—ãŸ</string> + <string name="notice_voip_started">VoIP会è°ãŒé–‹å§‹ã•ã‚Œã¾ã—ãŸ</string> + <string name="notice_voip_finished">VoIP会è°ãŒçµ‚了ã—ã¾ã—ãŸ</string> + + <string name="notice_avatar_changed_too">(ã‚¢ãƒã‚¿ãƒ¼ã‚‚変更ã•ã‚ŒãŸ)</string> + <string name="notice_room_name_removed">%1$s ãŒéƒ¨å±‹åを削除ã—ã¾ã—ãŸ</string> + <string name="notice_room_topic_removed">%1$s ãŒãƒ«ãƒ¼ãƒ トピックを削除ã—ã¾ã—ãŸ</string> + <string name="notice_profile_change_redacted">%1$s ãŒãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ« %2$s ã‚’æ›´æ–°ã—ã¾ã—ãŸ</string> + <string name="notice_room_third_party_invite">%1$s 㯠%2$s ã«éƒ¨å±‹ã«å‚åŠ ã™ã‚‹ã‚ˆã†æ‹›å¾…状をé€ã‚Šã¾ã—ãŸ</string> + <string name="notice_room_third_party_registered_invite">%1$sã¯%2$sã®æ‹›å¾…ã‚’å—ã‘入れã¾ã—ãŸ</string> + + <string name="notice_crypto_unable_to_decrypt">** 解èªã§ãã¾ã›ã‚“: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">é€ä¿¡è€…ã®ç«¯æœ«ã‹ã‚‰ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®ã‚ーãŒé€ä¿¡ã•ã‚Œã¦ã„ã¾ã›ã‚“。</string> + + <string name="could_not_redact">ä¿®æ£ã§ãã¾ã›ã‚“ã§ã—ãŸ</string> + <string name="unable_to_send_message">メッセージをé€ä¿¡ã§ãã¾ã›ã‚“</string> + + <string name="message_failed_to_upload">ç”»åƒã®ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸ</string> + + <string name="network_error">ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚¨ãƒ©ãƒ¼</string> + <string name="matrix_error">Matrixエラー</string> + + <string name="room_error_join_failed_empty_room">ç¾åœ¨ç©ºã®éƒ¨å±‹ã«å†å‚åŠ ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。</string> + + <string name="encrypted_message">æš—å·åŒ–ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸</string> + + <string name="medium_email">メールアドレス</string> + <string name="medium_phone_number">電話番å·</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-ko/strings.xml b/matrix-sdk-android/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..88c5e7d618eaf9a1ce85b6433983f7c7fe65df1f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml @@ -0,0 +1,167 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="notice_room_invite_no_invitee">%së‹˜ì˜ ì´ˆëŒ€</string> + <string name="verification_emoji_headphone">헤드í°</string> + <string name="summary_user_sent_image">%1$së‹˜ì´ ì‚¬ì§„ì„ ë³´ëƒˆìŠµë‹ˆë‹¤.</string> + <string name="summary_user_sent_sticker">%1$së‹˜ì´ ìŠ¤í‹°ì»¤ë¥¼ 보냈습니다.</string> + + <string name="notice_room_invite">%1$së‹˜ì´ %2$së‹˜ì„ ì´ˆëŒ€í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_invite_you">%1$së‹˜ì´ ë‹¹ì‹ ì„ ì´ˆëŒ€í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_join">%1$së‹˜ì´ ì°¸ê°€í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_leave">%1$së‹˜ì´ ë– ë‚¬ìŠµë‹ˆë‹¤</string> + <string name="notice_room_reject">%1$së‹˜ì´ ì´ˆëŒ€ë¥¼ 거부했습니다</string> + <string name="notice_room_kick">%1$së‹˜ì´ %2$së‹˜ì„ ì¶”ë°©í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_unban">%1$së‹˜ì´ %2$së‹˜ì˜ ì¶œìž… 금지를 풀었습니다</string> + <string name="notice_room_ban">%1$së‹˜ì´ %2$së‹˜ì„ ì¶œìž… 금지했습니다</string> + <string name="notice_room_withdraw">%1$së‹˜ì´ %2$së‹˜ì˜ ì´ˆëŒ€ë¥¼ 취소했습니다</string> + <string name="notice_avatar_url_changed">%1$së‹˜ì´ ì•„ë°”íƒ€ë¥¼ 변경했습니다</string> + <string name="notice_display_name_set">%1$së‹˜ì´ í‘œì‹œ ì´ë¦„ì„ %2$s(으)ë¡œ ì„¤ì •í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_display_name_changed_from">%1$së‹˜ì´ í‘œì‹œ ì´ë¦„ì„ %2$sì—ì„œ %3$s(으)ë¡œ 변경했습니다</string> + <string name="notice_display_name_removed">%1$së‹˜ì´ í‘œì‹œ ì´ë¦„ì„ ì‚ì œí–ˆìŠµë‹ˆë‹¤ (%2$s)</string> + <string name="notice_room_topic_changed">%1$së‹˜ì´ ì£¼ì œë¥¼ 다ìŒìœ¼ë¡œ 변경했습니다: %2$s</string> + <string name="notice_room_name_changed">%1$së‹˜ì´ ë°© ì´ë¦„ì„ ë‹¤ìŒìœ¼ë¡œ 변경했습니다: %2$s</string> + <string name="notice_placed_video_call">%së‹˜ì´ ì˜ìƒ 통화를 걸었습니다.</string> + <string name="notice_placed_voice_call">%së‹˜ì´ ìŒì„± 통화를 걸었습니다.</string> + <string name="notice_answered_call">%së‹˜ì´ ì „í™”ë¥¼ 받았습니다.</string> + <string name="notice_ended_call">%së‹˜ì´ ì „í™”ë¥¼ ëŠì—ˆìŠµë‹ˆë‹¤.</string> + <string name="notice_made_future_room_visibility">%1$së‹˜ì´ ì´í›„ %2$sì—게 ë°© 기ë¡ì„ 공개했습니다</string> + <string name="notice_room_visibility_invited">ì´ˆëŒ€ëœ ì‹œì 부터 ëª¨ë“ ë°© 구성ì›</string> + <string name="notice_room_visibility_joined">들어온 ì‹œì 부터 ëª¨ë“ ë°© 구성ì›</string> + <string name="notice_room_visibility_shared">ëª¨ë“ ë°© 구성ì›</string> + <string name="notice_room_visibility_world_readable">누구나.</string> + <string name="notice_room_visibility_unknown">ì•Œ 수 ì—†ìŒ (%s).</string> + <string name="notice_end_to_end">%1$së‹˜ì´ ì¢…ë‹¨ê°„ 암호화를 켰습니다 (%2$s)</string> + <string name="notice_room_update">%së‹˜ì´ ë°©ì„ ì—…ê·¸ë ˆì´ë“œí–ˆìŠµë‹ˆë‹¤.</string> + + <string name="notice_requested_voip_conference">%1$së‹˜ì´ VoIP 회ì˜ë¥¼ ìš”ì²í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_voip_started">VoIP 회ì˜ê°€ 시작했습니다</string> + <string name="notice_voip_finished">VoIP 회ì˜ê°€ ë났습니다</string> + + <string name="notice_avatar_changed_too">(ì•„ë°”íƒ€ë„ ë³€ê²½ë¨)</string> + <string name="notice_room_name_removed">%1$së‹˜ì´ ë°© ì´ë¦„ì„ ì‚ì œí–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_topic_removed">%1$së‹˜ì´ ë°© ì£¼ì œë¥¼ ì‚ì œí–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_event_redacted">메시지가 ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤</string> + <string name="notice_event_redacted_by">메시지가 %1$së‹˜ì— ì˜í•´ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤</string> + <string name="notice_event_redacted_with_reason">메시지가 ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤ [ì´ìœ : %1$s]</string> + <string name="notice_event_redacted_by_with_reason">메시지가 %1$së‹˜ì— ì˜í•´ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤ [ì´ìœ : %2$s]</string> + <string name="notice_profile_change_redacted">%1$së‹˜ì´ í”„ë¡œí•„ %2$sì„(를) ì—…ë°ì´íŠ¸í–ˆìŠµë‹ˆë‹¤</string> + <string name="notice_room_third_party_invite">%1$së‹˜ì´ %2$s님ì—게 ë°© 초대를 보냈습니다</string> + <string name="notice_room_third_party_registered_invite">%1$së‹˜ì´ %2$sì˜ ì´ˆëŒ€ë¥¼ 수ë½í–ˆìŠµë‹ˆë‹¤</string> + + <string name="notice_crypto_unable_to_decrypt">** 암호를 ë³µí˜¸í™”í• ìˆ˜ ì—†ìŒ: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">ë°œì‹ ì¸ì˜ 기기ì—ì„œ ì´ ë©”ì‹œì§€ì˜ í‚¤ë¥¼ 보내지 않았습니다.</string> + + <string name="could_not_redact">ê²€ì—´í• ìˆ˜ 없습니다</string> + <string name="unable_to_send_message">메시지를 보낼 수 없습니다</string> + + <string name="message_failed_to_upload">사진 ì—…ë¡œë“œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤</string> + + <string name="network_error">ë„¤íŠ¸ì›Œí¬ ì˜¤ë¥˜</string> + <string name="matrix_error">Matrix 오류</string> + + <string name="room_error_join_failed_empty_room">현재 빈 ë°©ì— ë‹¤ì‹œ 들어갈 수 없습니다.</string> + + <string name="encrypted_message">ì•”í˜¸í™”ëœ ë©”ì‹œì§€</string> + + <string name="medium_email">ì´ë©”ì¼ ì£¼ì†Œ</string> + <string name="medium_phone_number">ì „í™”ë²ˆí˜¸</string> + + <string name="room_displayname_invite_from">%sì—ì„œ 초대함</string> + <string name="room_displayname_room_invite">ë°© 초대</string> + + <string name="room_displayname_two_members">%1$s님과 %2$s님</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$s님 외 %2$d명</item> + </plurals> + + <string name="room_displayname_empty_room">빈 ë°©</string> + + + <string name="verification_emoji_dog">ê°œ</string> + <string name="verification_emoji_cat">ê³ ì–‘ì´</string> + <string name="verification_emoji_lion">사ìž</string> + <string name="verification_emoji_horse">ë§</string> + <string name="verification_emoji_unicorn">ìœ ë‹ˆì½˜</string> + <string name="verification_emoji_pig">ë¼ì§€</string> + <string name="verification_emoji_elephant">ì½”ë¼ë¦¬</string> + <string name="verification_emoji_rabbit">í† ë¼</string> + <string name="verification_emoji_panda">íŒë‹¤</string> + <string name="verification_emoji_rooster">수탉</string> + <string name="verification_emoji_penguin">íŽê·„</string> + <string name="verification_emoji_turtle">ê±°ë¶</string> + <string name="verification_emoji_fish">ë¬¼ê³ ê¸°</string> + <string name="verification_emoji_octopus">문어</string> + <string name="verification_emoji_butterfly">나비</string> + <string name="verification_emoji_flower">꽃</string> + <string name="verification_emoji_tree">나무</string> + <string name="verification_emoji_cactus">ì„ ì¸ìž¥</string> + <string name="verification_emoji_mushroom">버섯</string> + <string name="verification_emoji_globe">지구본</string> + <string name="verification_emoji_moon">달</string> + <string name="verification_emoji_cloud">구름</string> + <string name="verification_emoji_fire">불</string> + <string name="verification_emoji_banana">바나나</string> + <string name="verification_emoji_apple">사과</string> + <string name="verification_emoji_strawberry">딸기</string> + <string name="verification_emoji_corn">옥수수</string> + <string name="verification_emoji_pizza">피ìž</string> + <string name="verification_emoji_cake">ì¼€ì´í¬</string> + <string name="verification_emoji_heart">하트</string> + <string name="verification_emoji_smiley">웃ìŒ</string> + <string name="verification_emoji_robot">로봇</string> + <string name="verification_emoji_hat">모ìž</string> + <string name="verification_emoji_glasses">안경</string> + <string name="verification_emoji_wrench">스패너</string> + <string name="verification_emoji_santa">산타í´ë¡œìŠ¤</string> + <string name="verification_emoji_thumbsup">좋아요</string> + <string name="verification_emoji_umbrella">ìš°ì‚°</string> + <string name="verification_emoji_hourglass">모래시계</string> + <string name="verification_emoji_clock">시계</string> + <string name="verification_emoji_gift">ì„ ë¬¼</string> + <string name="verification_emoji_lightbulb">ì „êµ¬</string> + <string name="verification_emoji_book">ì±…</string> + <string name="verification_emoji_pencil">ì—°í•„</string> + <string name="verification_emoji_paperclip">í´ë¦½</string> + <string name="verification_emoji_scissors">가위</string> + <string name="verification_emoji_lock">ìžë¬¼ì‡ </string> + <string name="verification_emoji_key">ì—´ì‡ </string> + <string name="verification_emoji_hammer">ë§ì¹˜</string> + <string name="verification_emoji_telephone">ì „í™”ê¸°</string> + <string name="verification_emoji_flag">깃발</string> + <string name="verification_emoji_train">기차</string> + <string name="verification_emoji_bicycle">ìžì „ê±°</string> + <string name="verification_emoji_airplane">비행기</string> + <string name="verification_emoji_rocket">로켓</string> + <string name="verification_emoji_trophy">트로피</string> + <string name="verification_emoji_ball">ê³µ</string> + <string name="verification_emoji_guitar">기타</string> + <string name="verification_emoji_trumpet">트럼펫</string> + <string name="verification_emoji_bell">종</string> + <string name="verification_emoji_anchor">ë‹»</string> + <string name="verification_emoji_folder">í´ë”</string> + <string name="verification_emoji_pin">í•€</string> + + <string name="initial_sync_start_importing_account">초기 ë™ê¸°í™”: +\nê³„ì • ê°€ì ¸ì˜¤ëŠ” 중…</string> + <string name="initial_sync_start_importing_account_crypto">초기 ë™ê¸°í™”: +\n암호 ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_rooms">초기 ë™ê¸°í™”: +\në°© ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_joined_rooms">초기 ë™ê¸°í™”: +\n들어간 ë°© ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_invited_rooms">초기 ë™ê¸°í™”: +\nì´ˆëŒ€ë°›ì€ ë°© ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_left_rooms">초기 ë™ê¸°í™”: +\në– ë‚œ ë°© ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_groups">초기 ë™ê¸°í™”: +\n커뮤니티 ê°€ì ¸ì˜¤ëŠ” 중</string> + <string name="initial_sync_start_importing_account_data">초기 ë™ê¸°í™”: +\nê³„ì • ë°ì´í„° ê°€ì ¸ì˜¤ëŠ” 중</string> + + <string name="event_status_sending_message">메시지 보내는 중…</string> + <string name="clear_timeline_send_queue">ì „ì†¡ 대기 ì—´ 지우기</string> + + <string name="notice_room_third_party_revoked_invite">%1$së‹˜ì´ %2$s님ì—게 ë°©ì— ì°¸ê°€í•˜ë¼ê³ 보낸 초대를 취소했습니다</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-lt/strings.xml b/matrix-sdk-android/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b8672194081064be0c9039db2e97bac7aa611132 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lt/strings.xml @@ -0,0 +1,8 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s iÅ¡siuntÄ— atvaizdÄ….</string> + <string name="summary_user_sent_sticker">%1$s iÅ¡siuntÄ— lipdukÄ….</string> + + <string name="notice_room_invite_no_invitee">%s pakvietimas</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-lv/strings.xml b/matrix-sdk-android/src/main/res/values-lv/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b14cbb4b000a9a44a952e6400a3efe1a9c897e14 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lv/strings.xml @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s nosÅ«tÄ«ja attÄ“lu.</string> + + <string name="notice_room_invite_no_invitee">%s\'s uzaicinÄjums</string> + <string name="notice_room_invite">%1$s uzaicinÄja %2$s</string> + <string name="notice_room_invite_you">%1$s uzaicinÄja tevi</string> + <string name="notice_room_join">%1$s pievienojÄs</string> + <string name="notice_room_leave">%1$s atstÄja</string> + <string name="notice_room_reject">%1$s noraidÄ«ja uzaicinÄjumu</string> + <string name="notice_room_kick">%1$s \"izspÄ“ra\" ÄrÄ %2$s</string> + <string name="notice_room_unban">%1$s atbanoja (atcÄ“la pieejas liegumu) %2$s</string> + <string name="notice_room_ban">%1$s liedza pieeju (banoja) %2$s</string> + <string name="notice_room_withdraw">%1$s atsauca %2$s uzaicinÄjumu</string> + <string name="notice_avatar_url_changed">%1$s nomainÄ«ja profila attÄ“lu</string> + <string name="notice_display_name_set">%1$s uzstÄdÄ«ja redzamo vÄrdu uz %2$s</string> + <string name="notice_display_name_changed_from">%1$s nomainÄ«ja redzamo vÄrdu no %2$s uz %3$s</string> + <string name="notice_display_name_removed">%1$s dzÄ“sa savu redzamo vÄrdu (%2$s)</string> + <string name="notice_room_topic_changed">%1$s nomainÄ«ja tÄ“mas nosaukumu uz: %2$s</string> + <string name="notice_room_name_changed">%1$s nomainÄ«ja istabas nosaukumu uz: %2$s</string> + <string name="notice_placed_video_call">%s veica video zvanu.</string> + <string name="notice_placed_voice_call">%s veica audio zvanu.</string> + <string name="notice_answered_call">%s atbildÄ“ja zvanam.</string> + <string name="notice_ended_call">%s beidza zvanu.</string> + <string name="notice_made_future_room_visibility">%1$s padarÄ«ja istabas nÄkamo ziņu vÄ“sturi redzamu %2$s</string> + <string name="notice_room_visibility_invited">visi istabas biedri no brīža, kad tika uzaicinÄti.</string> + <string name="notice_room_visibility_joined">visi istabas biedri no brīža, kad tika pievienojuÅ¡ies.</string> + <string name="notice_room_visibility_shared">visi istabas biedri.</string> + <string name="notice_room_visibility_world_readable">ikviens.</string> + <string name="notice_room_visibility_unknown">nezinÄms (%s).</string> + <string name="notice_end_to_end">%1$s ieslÄ“dza ierÄ«ce-ierÄ«ce Å¡ifrÄ“Å¡anu (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s vÄ“las VoIP konferenci</string> + <string name="notice_voip_started">VoIP konference sÄkusies</string> + <string name="notice_voip_finished">VoIP konference ir beigusies</string> + + <string name="notice_avatar_changed_too">(arÄ« profila attÄ“ls mainÄ«jÄs)</string> + <string name="notice_room_name_removed">%1$s dzÄ“sa istabas nosaukumu</string> + <string name="notice_room_topic_removed">%1$s dzÄ“sa istabas tÄ“mas nosaukumu</string> + <string name="notice_profile_change_redacted">%1$s atjaunoja profila informÄciju %2$s</string> + <string name="notice_room_third_party_invite">%1$s nosÅ«tÄ«ja uzaicinÄjumu %2$s pievienoties istabai</string> + <string name="notice_room_third_party_registered_invite">%1$s apstiprinÄja uzaicinÄjumu priekÅ¡ %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Nav iespÄ“jams atkodÄ“t: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">SÅ«tÄ«tÄja ierÄ«ce mums nenosÅ«tÄ«ja atslÄ“gas priekÅ¡ Å¡Ä«s ziņas.</string> + + <string name="could_not_redact">NevarÄ“ja rediģēt</string> + <string name="unable_to_send_message">Nav iespÄ“jams nosÅ«tÄ«t ziņu</string> + + <string name="message_failed_to_upload">NeizdevÄs augÅ¡uplÄdÄ“t attÄ“lu</string> + + <string name="network_error">TÄ«kla kļūda</string> + <string name="matrix_error">Matrix kļūda</string> + + <string name="room_error_join_failed_empty_room">Å obrÄ«d nav iespÄ“jams atkÄrtoti pievienoties tukÅ¡ai istabai.</string> + + <string name="encrypted_message">Å ifrÄ“ta ziņa</string> + + <string name="medium_email">Epasta adrese</string> + <string name="medium_phone_number">Telefona numurs</string> + + <string name="room_displayname_invite_from">UzaicinÄjums no %s</string> + <string name="room_displayname_room_invite">UzaicinÄjums uz istabu</string> + <string name="room_displayname_two_members">%1$s un %2$s</string> + <string name="room_displayname_empty_room">TukÅ¡a istaba</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="zero">%1$s un 1 cits</item> + <item quantity="one">%1$s un %2$d citi</item> + <item quantity="other">%1$s un %2$d citu</item> + </plurals> + + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..22eb61f109c8958bb8ab1554e2d3ac638f276bb4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -0,0 +1,215 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s heeft een afbeelding gestuurd.</string> + + <string name="notice_room_invite_no_invitee">Uitnodiging van %s</string> + <string name="notice_room_invite">%1$s heeft %2$s uitgenodigd</string> + <string name="notice_room_invite_you">%1$s heeft u uitgenodigd</string> + <string name="notice_room_join">%1$s neemt nu deel aan het gesprek</string> + <string name="notice_room_leave">%1$s heeft het gesprek verlaten</string> + <string name="notice_room_reject">%1$s heeft de uitnodiging geweigerd</string> + <string name="notice_room_kick">%1$s heeft %2$s uit het gesprek verwijderd</string> + <string name="notice_room_unban">%1$s heeft %2$s ontbannen</string> + <string name="notice_room_ban">%1$s heeft %2$s verbannen</string> + <string name="notice_room_withdraw">%1$s heeft de uitnodiging van %2$s ingetrokken</string> + <string name="notice_avatar_url_changed">%1$s heeft zijn/haar avatar aangepast</string> + <string name="notice_display_name_set">%1$s heeft zijn/haar naam aangepast naar %2$s</string> + <string name="notice_display_name_changed_from">%1$s heeft zijn/haar naam aangepast van %2$s naar %3$s</string> + <string name="notice_display_name_removed">%1$s heeft zijn/haar naam verwijderd (%2$s)</string> + <string name="notice_room_topic_changed">%1$s heeft het onderwerp veranderd naar: %2$s</string> + <string name="notice_room_name_changed">%1$s heeft de gespreksnaam veranderd naar: %2$s</string> + <string name="notice_placed_video_call">%s heeft een video-oproep gemaakt.</string> + <string name="notice_placed_voice_call">%s heeft een spraakoproep gemaakt.</string> + <string name="notice_answered_call">%s heeft de oproep beantwoord.</string> + <string name="notice_ended_call">%s heeft opgehangen.</string> + <string name="notice_made_future_room_visibility">%1$s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %2$s</string> + <string name="notice_room_visibility_invited">alle deelnemers aan het gesprek, vanaf het punt dat ze zijn uitgenodigd.</string> + <string name="notice_room_visibility_joined">alle deelnemers aan het gesprek, vanaf het punt dat ze zijn toegetreden.</string> + <string name="notice_room_visibility_shared">alle deelnemers aan het gesprek.</string> + <string name="notice_room_visibility_world_readable">iedereen.</string> + <string name="notice_room_visibility_unknown">onbekend (%s).</string> + <string name="notice_end_to_end">%1$s heeft eind-tot-eind-versleuteling aangezet (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s heeft een VoIP-vergadering aangevraagd</string> + <string name="notice_voip_started">VoIP-vergadering gestart</string> + <string name="notice_voip_finished">VoIP-vergadering gestopt</string> + + <string name="notice_avatar_changed_too">(avatar is ook veranderd)</string> + <string name="notice_room_name_removed">%1$s heeft de gespreksnaam verwijderd</string> + <string name="notice_room_topic_removed">%1$s heeft het gespreksonderwerp verwijderd</string> + <string name="notice_profile_change_redacted">%1$s heeft zijn/haar profiel %2$s bijgewerkt</string> + <string name="notice_room_third_party_invite">%1$s heeft een uitnodiging naar %2$s gestuurd om het gesprek toe te treden</string> + <string name="notice_room_third_party_registered_invite">%1$s heeft de uitnodiging voor %2$s aanvaard</string> + + <string name="notice_crypto_unable_to_decrypt">** Kan niet ontsleutelen: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Het apparaat van de afzender heeft geen sleutels voor dit bericht gestuurd.</string> + + <!-- Room Screen --> + <string name="could_not_redact">Kon niet verwijderd worden</string> + <string name="unable_to_send_message">Kan bericht niet verzenden</string> + + <string name="message_failed_to_upload">Uploaden van de afbeelding mislukt</string> + + <!-- general errors --> + <string name="network_error">Netwerkfout</string> + <string name="matrix_error">Matrix-fout</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Het is momenteel niet mogelijk om een leeg gesprek opnieuw toe te treden.</string> + + <string name="encrypted_message">Versleuteld bericht</string> + + <!-- medium friendly name --> + <string name="medium_email">E-mailadres</string> + <string name="medium_phone_number">Telefoonnummer</string> + + <string name="summary_user_sent_sticker">%1$s heeft een sticker gestuurd.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Uitnodiging van %s</string> + <string name="room_displayname_room_invite">Gespreksuitnodiging</string> + <string name="room_displayname_two_members">%1$s en %2$s</string> + <string name="room_displayname_empty_room">Leeg gesprek</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s en 1 andere</item> + <item quantity="other">%1$s en %2$d anderen</item> + </plurals> + + + <string name="notice_event_redacted">Bericht verwijderd</string> + <string name="notice_event_redacted_by">Bericht verwijderd door %1$s</string> + <string name="notice_event_redacted_with_reason">Bericht verwijderd [reden: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Bericht verwijderd door %1$s [reden: %2$s]</string> + <string name="verification_emoji_dog">Hond</string> + <string name="verification_emoji_cat">Kat</string> + <string name="verification_emoji_lion">Leeuw</string> + <string name="verification_emoji_horse">Paard</string> + <string name="verification_emoji_unicorn">Eenhoorn</string> + <string name="verification_emoji_pig">Varken</string> + <string name="verification_emoji_elephant">Olifant</string> + <string name="verification_emoji_rabbit">Konijn</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Haan</string> + <string name="verification_emoji_penguin">Pinguïn</string> + <string name="verification_emoji_turtle">Schildpad</string> + <string name="verification_emoji_fish">Vis</string> + <string name="verification_emoji_octopus">Octopus</string> + <string name="verification_emoji_butterfly">Vlinder</string> + <string name="verification_emoji_flower">Bloem</string> + <string name="verification_emoji_tree">Boom</string> + <string name="verification_emoji_cactus">Cactus</string> + <string name="verification_emoji_mushroom">Paddenstoel</string> + <string name="verification_emoji_globe">Aardbol</string> + <string name="verification_emoji_moon">Maan</string> + <string name="verification_emoji_cloud">Wolk</string> + <string name="verification_emoji_fire">Vuur</string> + <string name="verification_emoji_banana">Banaan</string> + <string name="verification_emoji_apple">Appel</string> + <string name="verification_emoji_strawberry">Aardbei</string> + <string name="verification_emoji_corn">Maïs</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Taart</string> + <string name="verification_emoji_heart">Hart</string> + <string name="verification_emoji_smiley">Smiley</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Hoed</string> + <string name="verification_emoji_glasses">Bril</string> + <string name="verification_emoji_wrench">Moersleutel</string> + <string name="verification_emoji_santa">Kerstman</string> + <string name="verification_emoji_thumbsup">Duim omhoog</string> + <string name="verification_emoji_umbrella">Paraplu</string> + <string name="verification_emoji_hourglass">Zandloper</string> + <string name="verification_emoji_clock">Klok</string> + <string name="verification_emoji_gift">Cadeau</string> + <string name="verification_emoji_lightbulb">Gloeilamp</string> + <string name="verification_emoji_book">Boek</string> + <string name="verification_emoji_pencil">Potlood</string> + <string name="verification_emoji_paperclip">Paperclip</string> + <string name="verification_emoji_scissors">Schaar</string> + <string name="verification_emoji_lock">Slot</string> + <string name="verification_emoji_key">Sleutel</string> + <string name="verification_emoji_hammer">Hamer</string> + <string name="verification_emoji_telephone">Telefoon</string> + <string name="verification_emoji_flag">Vlag</string> + <string name="verification_emoji_train">Trein</string> + <string name="verification_emoji_bicycle">Fiets</string> + <string name="verification_emoji_airplane">Vliegtuig</string> + <string name="verification_emoji_rocket">Raket</string> + <string name="verification_emoji_trophy">Trofee</string> + <string name="verification_emoji_ball">Bal</string> + <string name="verification_emoji_guitar">Gitaar</string> + <string name="verification_emoji_trumpet">Trompet</string> + <string name="verification_emoji_bell">Bel</string> + <string name="verification_emoji_anchor">Anker</string> + <string name="verification_emoji_headphone">Koptelefoon</string> + <string name="verification_emoji_folder">Map</string> + <string name="verification_emoji_pin">Speld</string> + + <string name="initial_sync_start_importing_account">Initiële synchronisatie: +\nAccount wordt geïmporteerd…</string> + <string name="initial_sync_start_importing_account_crypto">Initiële synchronisatie: +\nCrypto wordt geïmporteerd</string> + <string name="initial_sync_start_importing_account_rooms">Initiële synchronisatie: +\nGesprekken worden geïmporteerd</string> + <string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisatie: +\nDeelgenomen gesprekken worden geïmporteerd</string> + <string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisatie: +\nUitgenodigde gesprekken worden geïmporteerd</string> + <string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisatie: +\nVerlaten gesprekken worden geïmporteerd</string> + <string name="initial_sync_start_importing_account_groups">Initiële synchronisatie: +\nGemeenschappen worden geïmporteerd</string> + <string name="initial_sync_start_importing_account_data">Initiële synchronisatie: +\nAccountgegevens worden geïmporteerd</string> + + <string name="notice_room_update">%s heeft dit gesprek opgewaardeerd.</string> + + <string name="event_status_sending_message">Bericht wordt verstuurd…</string> + <string name="clear_timeline_send_queue">Uitgaande wachtrij legen</string> + + <string name="notice_room_third_party_revoked_invite">%1$s heeft de uitnodiging voor %2$s om het gesprek toe te treden ingetrokken</string> + <string name="notice_room_invite_no_invitee_with_reason">Uitnodiging van %1$s. Reden: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s heeft %2$s uitgenodigd. Reden: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s heeft u uitgenodigd. Reden: %2$s</string> + <string name="notice_room_join_with_reason">%1$s neemt nu deel. Reden: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s is weggegaan. Reden: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s heeft de uitnodiging geweigerd. Reden: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s heeft %2$s verwijderd. Reden: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s heeft %2$s ontbannen. Reden: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s heeft %2$s verbannen. Reden: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s heeft %2$s een uitnodiging voor het gesprek gestuurd. Reden: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s heeft de uitnodiging voor %2$s ingetrokken. Reden: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s heeft de uitnodiging voor %2$s aanvaard. Reden: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s heeft de uitnodiging van %2$s ingetrokken. Reden: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s heeft %2$s als gespreksadres toegevoegd.</item> + <item quantity="other">%1$s heeft %2$s als gespreksadressen toegevoegd.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s heeft %2$s als gespreksadres verwijderd.</item> + <item quantity="other">%1$s heeft %3$s als gespreksadressen verwijderd.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s heeft %2$s als gespreksadres toegevoegd en %3$s verwijderd.</string> + + <string name="notice_room_canonical_alias_set">%1$s heeft het hoofdadres voor dit gesprek ingesteld op %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s heeft het hoofdadres voor dit gesprek verwijderd.</string> + + <string name="notice_room_guest_access_can_join">%1$s heeft gasten de toegang tot het gesprek verleend.</string> + <string name="notice_room_guest_access_forbidden">%1$s heeft gasten de toegang tot het gesprek verhinderd.</string> + + <string name="notice_end_to_end_ok">%1$s heeft eind-tot-eind-versleuteling ingeschakeld.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %2$s).</string> + + <string name="key_verification_request_fallback_message">%s vraagt om uw sleutel te verifiëren, maar uw cliënt biedt geen ondersteuning voor verificatie in het gesprek. U zult de verouderde sleutelverificatie moeten gebruiken om de sleutels te verifiëren.</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-nn/strings.xml b/matrix-sdk-android/src/main/res/values-nn/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..601cf4c9df01185f96ac7291cd453a31b5302ec7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nn/strings.xml @@ -0,0 +1,150 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="encrypted_message">Kryptert melding</string> + + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s sende eit bilæte.</string> + <string name="summary_user_sent_sticker">%1$s sende eit klistremerke.</string> + + <string name="notice_room_invite_no_invitee">%s si innbjoding</string> + <string name="notice_room_invite">%1$s inviterte %2$s</string> + <string name="notice_room_invite_you">%1$s inviterte deg</string> + <string name="notice_room_join">%1$s kom inn</string> + <string name="notice_room_leave">%1$s forlot rommet</string> + <string name="notice_room_reject">%1$s sa nei til innbjodingi</string> + <string name="notice_room_kick">%1$s sparka %2$s</string> + <string name="notice_room_unban">%1$s slapp %2$s inn att</string> + <string name="notice_room_ban">%1$s stengde %2$s ute</string> + <string name="notice_room_withdraw">%1$s tok attende %2$s si innbjoding</string> + <string name="notice_avatar_url_changed">%1$s byta avataren sin</string> + <string name="notice_display_name_set">%1$s sette visingsnamnet sitt som %2$s</string> + <string name="notice_display_name_changed_from">%1$s byta visingsnamnet sitt frÃ¥ %2$s til %3$s</string> + <string name="notice_display_name_removed">%1$s tok burt visingsnamnet sitt (%2$s)</string> + <string name="notice_room_topic_changed">%1$s gjorde emnet til: %2$s</string> + <string name="notice_room_name_changed">%1$s gjorde romnamnet til: %2$s</string> + <string name="notice_placed_video_call">%s starta ei videosamtala.</string> + <string name="notice_placed_voice_call">%s starta ein talesamtale.</string> + <string name="notice_answered_call">%s tok røyret.</string> + <string name="notice_ended_call">%s la pÃ¥ røyret.</string> + <string name="notice_made_future_room_visibility">%1$s gjorde den framtidige romsoga synleg for %2$s</string> + <string name="notice_room_visibility_invited">alle rommedlemmar, frÃ¥ dÃ¥ dei vart invitert inn.</string> + <string name="notice_room_visibility_joined">alle rommedlemmar, frÃ¥ dÃ¥ dei kom inn.</string> + <string name="notice_room_visibility_shared">alle rommedlemmar.</string> + <string name="notice_room_visibility_world_readable">kven som heldst.</string> + <string name="notice_room_visibility_unknown">uvisst (%s).</string> + <string name="notice_end_to_end">%1$s skrudde ende-til-ende-kryptering pÃ¥ (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s bad um ei VoIP-gruppasamtala</string> + <string name="notice_voip_started">VoIP-gruppasamtala er starta</string> + <string name="notice_voip_finished">VoIP-gruppasamtala er ferdug</string> + + <string name="notice_avatar_changed_too">(avataren vart au byta)</string> + <string name="notice_room_name_removed">%1$s tok burt romnamnet</string> + <string name="notice_room_topic_removed">%1$s tok burt romemnet</string> + <string name="notice_profile_change_redacted">%1$s gjorde um pÃ¥ skildringi si %2$s</string> + <string name="notice_room_third_party_invite">%1$s inviterte %2$s til rommet</string> + <string name="notice_room_third_party_registered_invite">%1$s sa ja til innbjodingi til %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Fekk ikkje til Ã¥ dekryptera: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Avsendareiningi hev ikkje sendt oss nyklane fyr denna meldingi.</string> + + <string name="could_not_redact">Kunde ikkje gjera um</string> + <string name="unable_to_send_message">Fekk ikkje Ã¥ senda meldingi</string> + + <string name="message_failed_to_upload">Fekk ikkje til Ã¥ lasta biletet upp</string> + + <string name="network_error">Noko gjekk gale med netverket</string> + <string name="matrix_error">Noko gjekk gale med Matrix</string> + + <string name="room_error_join_failed_empty_room">Det lèt seg fyrebils ikkje gjera Ã¥ fara inn att i eit tomt rom.</string> + + <string name="medium_email">Epostadresse</string> + <string name="medium_phone_number">Telefonnummer</string> + + <string name="room_displayname_invite_from">Innbjoding frÃ¥ %s</string> + <string name="room_displayname_room_invite">Rominnbjoding</string> + <string name="room_displayname_two_members">%1$s og %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s og 1 til</item> + <item quantity="other">%1$s og %2$d til</item> + </plurals> + + <string name="room_displayname_empty_room">Tomt rom</string> + + <string name="notice_event_redacted">Ei melding vart stroki</string> + <string name="notice_event_redacted_by">%1$s strauk meldingi</string> + <string name="notice_event_redacted_with_reason">Meldingi vart stroki [av di: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">%1$s strauk meldingi [av di: %2$s]</string> + <string name="verification_emoji_dog">Hund</string> + <string name="verification_emoji_cat">Katt</string> + <string name="verification_emoji_lion">Løva</string> + <string name="verification_emoji_horse">Hest</string> + <string name="verification_emoji_unicorn">Einhyrning</string> + <string name="verification_emoji_pig">Gris</string> + <string name="verification_emoji_elephant">Elefant</string> + <string name="verification_emoji_rabbit">Hare</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Hane</string> + <string name="verification_emoji_penguin">Pingvin</string> + <string name="verification_emoji_turtle">Skjoldpadda</string> + <string name="verification_emoji_fish">Fisk</string> + <string name="verification_emoji_octopus">Blekksprut</string> + <string name="verification_emoji_butterfly">Fivrelde</string> + <string name="verification_emoji_flower">Blome</string> + <string name="verification_emoji_tree">Tre</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Sopp</string> + <string name="verification_emoji_globe">Klote</string> + <string name="verification_emoji_moon">MÃ¥ne</string> + <string name="verification_emoji_cloud">Sky</string> + <string name="verification_emoji_fire">Eld</string> + <string name="verification_emoji_banana">Banan</string> + <string name="verification_emoji_apple">Eple</string> + <string name="verification_emoji_strawberry">Jordbær</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Kaka</string> + <string name="verification_emoji_heart">Hjarta</string> + <string name="verification_emoji_smiley">Smilandlit</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Hatt</string> + <string name="verification_emoji_glasses">Brillor</string> + <string name="verification_emoji_wrench">Skiftenykel</string> + <string name="verification_emoji_santa">Nissen</string> + <string name="verification_emoji_thumbsup">Tumalen Upp</string> + <string name="verification_emoji_umbrella">Regnskjold</string> + <string name="verification_emoji_hourglass">Timeglas</string> + <string name="verification_emoji_clock">Ur</string> + <string name="verification_emoji_gift">GÃ¥va</string> + <string name="verification_emoji_lightbulb">Ljospera</string> + <string name="verification_emoji_book">Bok</string> + <string name="verification_emoji_pencil">Blyant</string> + <string name="verification_emoji_paperclip">Binders</string> + <string name="verification_emoji_scissors">Saks</string> + <string name="verification_emoji_lock">LÃ¥s</string> + <string name="verification_emoji_key">Nykel</string> + <string name="verification_emoji_hammer">Hamar</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Flagg</string> + <string name="verification_emoji_train">Tog</string> + <string name="verification_emoji_bicycle">Sykkel</string> + <string name="verification_emoji_airplane">Flyg</string> + <string name="verification_emoji_rocket">Rakett</string> + <string name="verification_emoji_trophy">Pokal</string> + <string name="verification_emoji_ball">Ball</string> + <string name="verification_emoji_guitar">Gitar</string> + <string name="verification_emoji_trumpet">Trompet</string> + <string name="verification_emoji_bell">Klokka</string> + <string name="verification_emoji_anchor">Ankar</string> + <string name="verification_emoji_headphone">Hodetelefon</string> + <string name="verification_emoji_folder">Mappa</string> + <string name="verification_emoji_pin">NÃ¥l</string> + + <string name="notice_room_update">%s oppgraderte rommet.</string> + + <string name="clear_timeline_send_queue">Nullstill sendingskø</string> + + <string name="notice_room_leave_with_reason">%1$s forlot rommet. Grunn: %2$s</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc380516b75e7ab6aa6ebae1a6131b1eb00d5ade --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -0,0 +1,169 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s wysÅ‚aÅ‚(a) zdjÄ™cie.</string> + + <string name="notice_room_invite_no_invitee">Zaproszenie od %s</string> + <string name="notice_room_invite">%1$s zaprosiÅ‚(a) %2$s</string> + <string name="notice_room_invite_you">%1$s zaprosiÅ‚(a) CiÄ™</string> + <string name="notice_room_join">%1$s doÅ‚Ä…czyÅ‚(a)</string> + <string name="notice_room_leave">%1$s opuÅ›ciÅ‚(a)</string> + <string name="notice_room_reject">%1$s odrzuciÅ‚(a) zaproszenie</string> + <string name="notice_room_kick">%1$s wyrzuciÅ‚(a) %2$s</string> + <string name="notice_room_unban">%1$s odblokowaÅ‚(a) %2$s</string> + <string name="notice_room_ban">%1$s zablokowaÅ‚(a) %2$s</string> + <string name="notice_avatar_url_changed">%1$s zmieniÅ‚(a) awatar</string> + <string name="notice_display_name_set">%1$s zmieniÅ‚(a) wyÅ›wietlanÄ… nazwÄ™ na %2$s</string> + <string name="notice_display_name_changed_from">%1$s zmieniÅ‚(a) wyÅ›wietlanÄ… nazwÄ™ z %2$s na %3$s</string> + <string name="notice_display_name_removed">%1$s usunÄ…Å‚(-ęła) swojÄ… wyÅ›wietlanÄ… nazwÄ™ (%2$s)</string> + <string name="notice_room_topic_changed">%1$s zmieniÅ‚(a) temat na: %2$s</string> + <string name="unable_to_send_message">Nie można wysÅ‚ać wiadomoÅ›ci</string> + + <string name="message_failed_to_upload">PrzesyÅ‚anie zdjÄ™cia nie powiodÅ‚o siÄ™</string> + + <string name="network_error">BÅ‚Ä…d sieci</string> + <string name="matrix_error">BÅ‚Ä…d Matrixa</string> + + <string name="encrypted_message">Wiadomość zaszyfrowana</string> + + <string name="medium_email">Adres e-mail</string> + <string name="medium_phone_number">Numer telefonu</string> + + <string name="notice_room_visibility_shared">wszyscy czÅ‚onkowie pokoju.</string> + <string name="notice_room_visibility_world_readable">wszyscy.</string> + <string name="notice_room_name_changed">%1$s zmieniÅ‚(a) nazwÄ™ pokoju na: %2$s</string> + <string name="notice_ended_call">%s zakoÅ„czyÅ‚(a) rozmowÄ™.</string> + <string name="notice_room_name_removed">%1$s usunÄ…Å‚(-ęła) nazwÄ™ pokoju</string> + <string name="notice_room_topic_removed">%1$s usunÄ…Å‚(-ęła) temat pokoju</string> + <string name="summary_user_sent_sticker">%1$s wysÅ‚aÅ‚(a) naklejkÄ™.</string> + + <string name="notice_end_to_end">%1$s wÅ‚Ä…czyÅ‚(a) szyfrowanie end-to-end (%2$s)</string> + + <string name="notice_room_withdraw">%1$s wycofaÅ‚(a) zaproszenie %2$s</string> + <string name="notice_answered_call">%s odebraÅ‚(a) poÅ‚Ä…czenie.</string> + <string name="notice_avatar_changed_too">(awatar też zostaÅ‚ zmieniony)</string> + + <string name="room_displayname_invite_from">Zaproszenie od %s</string> + <string name="room_displayname_room_invite">Zaproszenie do pokoju</string> + <string name="room_displayname_two_members">%1$s i %2$s</string> + <string name="room_displayname_empty_room">Pusty pokój</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s i jeden inny</item> + <item quantity="few">%1$s i kilku innych</item> + <item quantity="many">%1$s i %2$d innych</item> + <item quantity="other" /> + </plurals> + + <string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string> + <string name="notice_placed_video_call">%s wykonaÅ‚(a) rozmowÄ™ wideo.</string> + <string name="notice_placed_voice_call">%s wykonaÅ‚(a) poÅ‚Ä…czenie gÅ‚osowe.</string> + <string name="notice_made_future_room_visibility">%1$s uczyniÅ‚(a) przyszÅ‚Ä… historiÄ™ pokoju widocznÄ… dla %2$s</string> + <string name="notice_room_visibility_invited">wszyscy czÅ‚onkowie pokoju, od momentu w którym zostali zaproszeni.</string> + <string name="notice_room_visibility_joined">wszyscy czÅ‚onkowie pokoju, od momentu w którym doÅ‚Ä…czyli.</string> + <string name="notice_room_visibility_unknown">nieznane (%s).</string> + <string name="notice_requested_voip_conference">%1$s zażądaÅ‚(a) grupowego poÅ‚Ä…czenia VoIP</string> + <string name="notice_voip_started">RozpoczÄ™to grupowe poÅ‚Ä…czenie gÅ‚osowe VoIP</string> + <string name="notice_voip_finished">ZakoÅ„czono grupowe poÅ‚Ä…czenie gÅ‚osowe VoIP</string> + + <string name="notice_profile_change_redacted">%1$s zaktualizowaÅ‚ swój profil %2$s</string> + <string name="notice_room_third_party_invite">%1$s wysÅ‚aÅ‚(a) zaproszenie do %2$s aby doÅ‚Ä…czyÅ‚(a) do tego pokoju</string> + <string name="notice_room_third_party_registered_invite">%1$s zaakceptowaÅ‚(a) zaproszenie dla %2$s</string> + + <string name="notice_crypto_error_unkwown_inbound_session_id">UrzÄ…dzenie nadawcy nie wysÅ‚aÅ‚o nam kluczy do tej wiadomoÅ›ci.</string> + + <string name="could_not_redact">Nie można zredagować</string> + <string name="room_error_join_failed_empty_room">Obecnie nie jest możliwe ponowne doÅ‚Ä…czenie do pustego pokoju.</string> + + <string name="notice_event_redacted">Wiadomość usuniÄ™ta</string> + <string name="notice_event_redacted_by">Wiadomość usuniÄ™ta przez %1$s</string> + <string name="notice_event_redacted_with_reason">Wiadomość usuniÄ™ta [powód: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Wiadomość usuniÄ™ta przez %1$s [powód: %2$s]</string> + <string name="verification_emoji_dog">Pies</string> + <string name="verification_emoji_cat">Kot</string> + <string name="verification_emoji_lion">Lew</string> + <string name="verification_emoji_horse">KoÅ„</string> + <string name="verification_emoji_unicorn">Jednorożec</string> + <string name="verification_emoji_pig">Åšwinia</string> + <string name="verification_emoji_elephant">SÅ‚oÅ„</string> + <string name="verification_emoji_rabbit">Królik</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kogut</string> + <string name="verification_emoji_penguin">Pingwin</string> + <string name="verification_emoji_turtle">Żółw</string> + <string name="verification_emoji_fish">Ryba</string> + <string name="verification_emoji_octopus">OÅ›miornica</string> + <string name="verification_emoji_butterfly">Motyl</string> + <string name="verification_emoji_flower">Kwiat</string> + <string name="verification_emoji_tree">Drzewo</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Grzyb</string> + <string name="verification_emoji_moon">Księżyc</string> + <string name="verification_emoji_cloud">Chmura</string> + <string name="verification_emoji_fire">OgieÅ„</string> + <string name="verification_emoji_banana">Banan</string> + <string name="verification_emoji_apple">JabÅ‚ko</string> + <string name="verification_emoji_strawberry">Truskawka</string> + <string name="verification_emoji_corn">Kukurydza</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Ciasto</string> + <string name="verification_emoji_heart">Serce</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Kapelusz</string> + <string name="verification_emoji_glasses">Okulary</string> + <string name="verification_emoji_umbrella">Parasol</string> + <string name="verification_emoji_hourglass">Klepsydra</string> + <string name="verification_emoji_clock">Zegar</string> + <string name="verification_emoji_lightbulb">Å»arówka</string> + <string name="verification_emoji_book">Książka</string> + <string name="verification_emoji_pencil">Ołówek</string> + <string name="verification_emoji_paperclip">Spinacz</string> + <string name="verification_emoji_scissors">Nożyczki</string> + <string name="verification_emoji_key">Klucz</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Flaga</string> + <string name="verification_emoji_train">PociÄ…g</string> + <string name="verification_emoji_bicycle">Rower</string> + <string name="verification_emoji_airplane">Samolot</string> + <string name="verification_emoji_rocket">Rakieta</string> + <string name="verification_emoji_trophy">Trofeum</string> + <string name="verification_emoji_guitar">Gitara</string> + <string name="verification_emoji_trumpet">TrÄ…bka</string> + <string name="verification_emoji_bell">Dzwonek</string> + <string name="verification_emoji_anchor">Kotwica</string> + <string name="verification_emoji_headphone">SÅ‚uchawki</string> + <string name="verification_emoji_folder">Folder</string> + <string name="verification_emoji_pin">Pinezka</string> + + <string name="verification_emoji_globe">Ziemia</string> + <string name="verification_emoji_smiley">UÅ›miech</string> + <string name="verification_emoji_wrench">Klucz francuski</string> + <string name="verification_emoji_santa">MikoÅ‚aj</string> + <string name="verification_emoji_gift">Prezent</string> + <string name="verification_emoji_hammer">MÅ‚otek</string> + <string name="notice_room_update">%s zakutalizowaÅ‚(a) ten pokój.</string> + + <string name="verification_emoji_thumbsup">Kciuk w górÄ™</string> + <string name="verification_emoji_lock">Zamek</string> + <string name="verification_emoji_ball">PiÅ‚ka</string> + <string name="initial_sync_start_importing_account">Synchronizacja poczÄ…tkowa: +\nImportowanie konta…</string> + <string name="initial_sync_start_importing_account_crypto">Synchronizacja poczÄ…tkowa: +\nImportowanie kryptografii</string> + <string name="initial_sync_start_importing_account_rooms">Synchronizacja poczÄ…tkowa: +\nImportowanie Pokoi</string> + <string name="initial_sync_start_importing_account_joined_rooms">Synchronizacja poczÄ…tkowa: +\nImportowanie doÅ‚Ä…czonych Pokoi</string> + <string name="initial_sync_start_importing_account_invited_rooms">Synchronizacja poczÄ…tkowa: +\nImportowanie zaproszonych Pokoi</string> + <string name="initial_sync_start_importing_account_left_rooms">Synchronizacja poczÄ…tkowa: +\nImportowanie opuszczonych Pokoi</string> + <string name="initial_sync_start_importing_account_groups">Synchronizacja poczÄ…tkowa: +\nImportowanie SpoÅ‚ecznoÅ›ci</string> + <string name="initial_sync_start_importing_account_data">Synchronizacja poczÄ…tkowa: +\nImportowanie danych Konta</string> + + <string name="event_status_sending_message">WysyÅ‚anie wiadomoÅ›ci…</string> + <string name="clear_timeline_send_queue">Wyczyść kolejkÄ™ wysyÅ‚ania</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7c5d6fe583665dca955a23817e7df08a8923d971 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,309 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s enviou uma foto.</string> + + <string name="notice_room_invite_no_invitee">convite de %s</string> + <string name="notice_room_invite">%1$s convidou %2$s</string> + <string name="notice_room_invite_you">%1$s convidou você</string> + <string name="notice_room_join">%1$s entrou na sala</string> + <string name="notice_room_leave">%1$s saiu da sala</string> + <string name="notice_room_reject">%1$s recusou o convite</string> + <string name="notice_room_kick">%1$s removeu %2$s</string> + <string name="notice_room_unban">%1$s removeu o banimento de %2$s</string> + <string name="notice_room_ban">%1$s baniu %2$s</string> + <string name="notice_room_withdraw">%1$s desfez o convite a %2$s</string> + <string name="notice_avatar_url_changed">%1$s alterou a foto de perfil</string> + <string name="notice_display_name_set">%1$s definiu o nome e sobrenome como %2$s</string> + <string name="notice_display_name_changed_from">%1$s alterou o nome e sobrenome de %2$s para %3$s</string> + <string name="notice_display_name_removed">%1$s removeu o nome e sobrenome (era %2$s)</string> + <string name="notice_room_topic_changed">%1$s alterou a descrição para: %2$s</string> + <string name="notice_room_name_changed">%1$s alterou o nome da sala para: %2$s</string> + <string name="notice_placed_video_call">%s iniciou uma chamada de vÃdeo.</string> + <string name="notice_placed_voice_call">%s iniciou uma chamada de voz.</string> + <string name="notice_answered_call">%s aceitou a chamada.</string> + <string name="notice_ended_call">%s encerrou a chamada.</string> + <string name="notice_made_future_room_visibility">%1$s deixou o histórico futuro da sala visÃvel para %2$s</string> + <string name="notice_room_visibility_invited">todos os membros da sala, a partir do momento em que foram convidados.</string> + <string name="notice_room_visibility_joined">todos os membros da sala, a partir do momento em que entraram nela.</string> + <string name="notice_room_visibility_shared">todos os membros da sala.</string> + <string name="notice_room_visibility_world_readable">qualquer pessoa.</string> + <string name="notice_room_visibility_unknown">desconhecido (%s).</string> + <string name="notice_end_to_end">%1$s ativou a criptografia de ponta a ponta (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s deseja iniciar uma chamada em grupo</string> + <string name="notice_voip_started">Chamada em grupo iniciada</string> + <string name="notice_voip_finished">Chamada em grupo encerrada</string> + + <string name="notice_avatar_changed_too">(a foto de perfil também foi alterada)</string> + <string name="notice_room_name_removed">%1$s removeu o nome da sala</string> + <string name="notice_room_topic_removed">%1$s removeu a descrição da sala</string> + <string name="notice_profile_change_redacted">%1$s atualizou o perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s enviou um convite para %2$s entrar na sala</string> + <string name="notice_room_third_party_registered_invite">%1$s aceitou o convite para %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Não foi possÃvel descriptografar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">O aparelho do remetente não nos enviou as chaves para esta mensagem.</string> + + <!-- Room Screen --> + <string name="could_not_redact">Não foi possÃvel redigir</string> + <string name="unable_to_send_message">Não foi possÃvel enviar a mensagem</string> + + <string name="message_failed_to_upload">O envio da imagem falhou</string> + + <!-- general errors --> + <string name="network_error">Erro de conexão à internet</string> + <string name="matrix_error">Erro no servidor Matrix</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Atualmente, não é possÃvel entrar novamente em uma sala vazia.</string> + + <string name="encrypted_message">Mensagem criptografada</string> + + <!-- medium friendly name --> + <string name="medium_email">Endereço de e-mail</string> + <string name="medium_phone_number">Número de telefone</string> + + + <string name="summary_user_sent_sticker">%1$s enviou uma figurinha.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Convite de %s</string> + <string name="room_displayname_room_invite">Convite para sala</string> + <string name="room_displayname_two_members">%1$s e %2$s</string> + <string name="room_displayname_empty_room">Sala vazia</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s e 1 outro</item> + <item quantity="other">%1$s e %2$d outros</item> + </plurals> + + + <string name="summary_you_sent_image">Você enviou uma foto.</string> + <string name="summary_you_sent_sticker">Você enviou uma figurinha.</string> + + <string name="notice_room_invite_no_invitee_by_you">Seu convite</string> + <string name="notice_room_created">%1$s criou a sala</string> + <string name="notice_room_created_by_you">Você criou a sala</string> + <string name="notice_room_invite_by_you">Você convidou %1$s</string> + <string name="notice_room_join_by_you">Você entrou na sala</string> + <string name="notice_room_leave_by_you">Você saiu da sala</string> + <string name="notice_room_reject_by_you">Você recusou o convite</string> + <string name="notice_room_kick_by_you">Você removeu %1$s</string> + <string name="notice_room_unban_by_you">Você removeu o banimento de %1$s</string> + <string name="notice_room_ban_by_you">Você baniu %1$s</string> + <string name="notice_room_withdraw_by_you">Você desfez o convite a %1$s</string> + <string name="notice_avatar_url_changed_by_you">Você alterou a sua foto de perfil</string> + <string name="notice_display_name_set_by_you">Você definiu o seu nome e sobrenome como %1$s</string> + <string name="notice_display_name_changed_from_by_you">Você alterou o seu nome e sobrenome de %1$s para %2$s</string> + <string name="notice_display_name_removed_by_you">Você removeu o seu nome e sobrenome (era %1$s)</string> + <string name="notice_room_topic_changed_by_you">Você alterou a descrição para: %1$s</string> + <string name="notice_room_avatar_changed">%1$s alterou a foto da sala</string> + <string name="notice_room_avatar_changed_by_you">Você alterou a foto da sala</string> + <string name="notice_room_name_changed_by_you">Você alterou o nome da sala para: %1$s</string> + <string name="notice_placed_video_call_by_you">Você iniciou uma chamada de vÃdeo.</string> + <string name="notice_placed_voice_call_by_you">Você iniciou uma chamada de voz.</string> + <string name="notice_call_candidates">%s enviou dados para configurar a chamada.</string> + <string name="notice_call_candidates_by_you">Você enviou dados para configurar a chamada.</string> + <string name="notice_answered_call_by_you">Você aceitou a chamada.</string> + <string name="notice_ended_call_by_you">Você encerrou a chamada.</string> + <string name="notice_made_future_room_visibility_by_you">Você deixou o histórico futuro da sala visÃvel para %1$s</string> + <string name="notice_end_to_end_by_you">Você ativou a criptografia de ponta a ponta (%1$s)</string> + <string name="notice_room_update">%s atualizou esta sala.</string> + <string name="notice_room_update_by_you">Você atualizou esta sala.</string> + + <string name="notice_requested_voip_conference_by_you">Você solicitou uma chamada em grupo</string> + <string name="notice_room_name_removed_by_you">Você removeu o nome da sala</string> + <string name="notice_room_topic_removed_by_you">Você removeu a descrição da sala</string> + <string name="notice_room_avatar_removed">%1$s removeu a foto da sala</string> + <string name="notice_room_avatar_removed_by_you">Você removeu a foto da sala</string> + <string name="notice_event_redacted">Mensagem removida</string> + <string name="notice_event_redacted_by">Mensagem removida por %1$s</string> + <string name="notice_event_redacted_with_reason">Mensagem removida [motivo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mensagem removida por %1$s [motivo: %2$s]</string> + <string name="notice_profile_change_redacted_by_you">Você atualizou o seu perfil %1$s</string> + <string name="notice_room_third_party_invite_by_you">Você enviou um convite para %1$s entrar na sala</string> + <string name="notice_room_third_party_revoked_invite">%1$s cancelou o convite a %2$s para entrar na sala</string> + <string name="notice_room_third_party_revoked_invite_by_you">Você cancelou o convite a %1$s para entrar na sala</string> + <string name="notice_room_third_party_registered_invite_by_you">Você aceitou o convite para %1$s</string> + + <string name="notice_widget_added">%1$s adicionou o widget %2$s</string> + <string name="notice_widget_added_by_you">Você adicionou o widget %1$s</string> + <string name="notice_widget_removed">%1$s removeu o widget %2$s</string> + <string name="notice_widget_removed_by_you">Você removeu o widget %1$s</string> + <string name="notice_widget_modified">%1$s editou o widget %2$s</string> + <string name="notice_widget_modified_by_you">Você editou o widget %1$s</string> + + <string name="power_level_admin">Administrador</string> + <string name="power_level_moderator">Moderador</string> + <string name="power_level_default">Padrão</string> + <string name="power_level_custom">Personalizado (%1$d)</string> + <string name="power_level_custom_no_value">Personalizado</string> + + <string name="notice_power_level_changed_by_you">Você alterou o nÃvel de permissão de %1$s.</string> + <string name="notice_power_level_changed">%1$s alterou o nÃvel de permissão de %2$s.</string> + <string name="notice_power_level_diff">%1$s de %2$s para %3$s</string> + + <string name="verification_emoji_dog">Cachorro</string> + <string name="verification_emoji_cat">Gato</string> + <string name="verification_emoji_lion">Leão</string> + <string name="verification_emoji_horse">Cavalo</string> + <string name="verification_emoji_unicorn">Unicórnio</string> + <string name="verification_emoji_pig">Porco</string> + <string name="verification_emoji_elephant">Elefante</string> + <string name="verification_emoji_rabbit">Coelho</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Galo</string> + <string name="verification_emoji_penguin">Pinguim</string> + <string name="verification_emoji_turtle">Tartaruga</string> + <string name="verification_emoji_fish">Peixe</string> + <string name="verification_emoji_octopus">Polvo</string> + <string name="verification_emoji_butterfly">Borboleta</string> + <string name="verification_emoji_flower">Flor</string> + <string name="verification_emoji_tree">Ãrvore</string> + <string name="verification_emoji_cactus">Cacto</string> + <string name="verification_emoji_mushroom">Cogumelo</string> + <string name="verification_emoji_globe">Globo</string> + <string name="verification_emoji_moon">Lua</string> + <string name="verification_emoji_cloud">Nuvem</string> + <string name="verification_emoji_fire">Fogo</string> + <string name="verification_emoji_banana">Banana</string> + <string name="verification_emoji_apple">Maçã</string> + <string name="verification_emoji_strawberry">Morango</string> + <string name="verification_emoji_corn">Milho</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Bolo</string> + <string name="verification_emoji_heart">Coração</string> + <string name="verification_emoji_smiley">Sorriso</string> + <string name="verification_emoji_robot">Robô</string> + <string name="verification_emoji_hat">Chapéu</string> + <string name="verification_emoji_glasses">Óculos</string> + <string name="verification_emoji_wrench">Chave inglesa</string> + <string name="verification_emoji_santa">Papai-noel</string> + <string name="verification_emoji_thumbsup">Joinha</string> + <string name="verification_emoji_umbrella">Guarda-chuva</string> + <string name="verification_emoji_hourglass">Ampulheta</string> + <string name="verification_emoji_clock">Relógio</string> + <string name="verification_emoji_gift">Presente</string> + <string name="verification_emoji_lightbulb">Lâmpada</string> + <string name="verification_emoji_book">Livro</string> + <string name="verification_emoji_pencil">Lápis</string> + <string name="verification_emoji_paperclip">Clipe de papel</string> + <string name="verification_emoji_scissors">Tesoura</string> + <string name="verification_emoji_lock">Cadeado</string> + <string name="verification_emoji_key">Chave</string> + <string name="verification_emoji_hammer">Martelo</string> + <string name="verification_emoji_telephone">Telefone</string> + <string name="verification_emoji_flag">Bandeira</string> + <string name="verification_emoji_train">Trem</string> + <string name="verification_emoji_bicycle">Bicicleta</string> + <string name="verification_emoji_airplane">Avião</string> + <string name="verification_emoji_rocket">Foguete</string> + <string name="verification_emoji_trophy">Troféu</string> + <string name="verification_emoji_ball">Bola</string> + <string name="verification_emoji_guitar">Guitarra</string> + <string name="verification_emoji_trumpet">Trombeta</string> + <string name="verification_emoji_bell">Sino</string> + <string name="verification_emoji_anchor">Âncora</string> + <string name="verification_emoji_headphone">Fones de ouvido</string> + <string name="verification_emoji_folder">Pasta</string> + <string name="verification_emoji_pin">Alfinete</string> + + <string name="initial_sync_start_importing_account">Primeira sincronização:↵ +\nImportando a conta…</string> + <string name="initial_sync_start_importing_account_crypto">Primeira sincronização:↵ +\nImportando as chaves de criptografia</string> + <string name="initial_sync_start_importing_account_rooms">Primeira sincronização:↵ +\nImportando as salas</string> + <string name="initial_sync_start_importing_account_joined_rooms">Primeira sincronização:↵ +\nImportando as salas em que você entrou</string> + <string name="initial_sync_start_importing_account_invited_rooms">Primeira sincronização:↵ +\nImportando as salas em que você foi convidado</string> + <string name="initial_sync_start_importing_account_left_rooms">Primeira sincronização:↵ +\nImportando as salas em que você saiu</string> + <string name="initial_sync_start_importing_account_groups">Primeira sincronização:↵ +\nImportando as comunidades</string> + <string name="initial_sync_start_importing_account_data">Primeira sincronização:↵ +\nImportando os dados da conta</string> + + <string name="event_status_sending_message">Enviando mensagem…</string> + <string name="clear_timeline_send_queue">Limpar a fila de envio</string> + + <string name="notice_room_invite_no_invitee_with_reason">Convite de %1$s. Motivo: %2$s</string> + <string name="notice_room_invite_no_invitee_with_reason_by_you">O seu convite. Motivo: %1$s</string> + <string name="notice_room_invite_with_reason">%1$s convidou %2$s. Motivo: %3$s</string> + <string name="notice_room_invite_with_reason_by_you">Você convidou %1$s. Motivo: %2$s</string> + <string name="notice_room_invite_you_with_reason">%1$s convidou você. Motivo: %2$s</string> + <string name="notice_room_join_with_reason">%1$s entrou na sala. Motivo: %2$s</string> + <string name="notice_room_join_with_reason_by_you">Você entrou na sala. Motivo: %1$s</string> + <string name="notice_room_leave_with_reason">%1$s saiu da sala. Motivo: %2$s</string> + <string name="notice_room_leave_with_reason_by_you">Você saiu da sala. Motivo: %1$s</string> + <string name="notice_room_reject_with_reason">%1$s recusou o convite. Motivo: %2$s</string> + <string name="notice_room_reject_with_reason_by_you">Você recusou o convite. Motivo: %1$s</string> + <string name="notice_room_kick_with_reason">%1$s removeu %2$s. Motivo: %3$s</string> + <string name="notice_room_kick_with_reason_by_you">Você removeu %1$s. Motivo: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s removeu o banimento de %2$s. Motivo: %3$s</string> + <string name="notice_room_unban_with_reason_by_you">Você removeu o banimento de %1$s. Motivo: %2$s</string> + <string name="notice_room_ban_with_reason">%1$s baniu %2$s. Motivo: %3$s</string> + <string name="notice_room_ban_with_reason_by_you">Você baniu %1$s. Motivo: %2$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s enviou um convite para %2$s entrar na sala. Motivo: %3$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Você enviou um convite para %1$s entrar na sala. Motivo: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s revogou o convite para %2$s entrar na sala. Motivo: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Você revogou o convite para %1$s entrar na sala. Motivo: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s aceitou o convite para %2$s. Motivo: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Você aceitou o convite para %1$s. Motivo: %2$s</string> + <string name="notice_room_withdraw_with_reason">%1$s desfez o convite de %2$s. Motivo: %3$s</string> + <string name="notice_room_withdraw_with_reason_by_you">Você desfez o convite de %1$s. Motivo: %2$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s adicionou %2$s como um endereço desta sala.</item> + <item quantity="other">%1$s adicionou %2$s como endereços desta sala.</item> + </plurals> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">Você adicionou %1$s como um endereço desta sala.</item> + <item quantity="other">Você adicionou %1$s como endereços desta sala.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s removeu %2$s como um endereço desta sala.</item> + <item quantity="other">%1$s removeu %3$s como endereços desta sala.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">Você removeu %1$s como um endereço desta sala.</item> + <item quantity="other">Você removeu %2$s como endereços desta sala.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s adicionou %2$s e removeu %3$s como endereços desta sala.</string> + <string name="notice_room_aliases_added_and_removed_by_you">Você adicionou %1$s e removeu %2$s como endereços desta sala.</string> + + <string name="notice_room_canonical_alias_set">%1$s definiu o endereço principal desta sala como %2$s.</string> + <string name="notice_room_canonical_alias_set_by_you">Você definiu o endereço principal desta sala como %1$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s removeu o endereço principal desta sala.</string> + <string name="notice_room_canonical_alias_unset_by_you">Você removeu o endereço principal desta sala.</string> + + <string name="notice_room_guest_access_can_join">%1$s permitiu que convidados entrem na sala.</string> + <string name="notice_room_guest_access_can_join_by_you">Você permitiu que convidados entrem na sala.</string> + <string name="notice_room_guest_access_forbidden">%1$s impediu que convidados entrem na sala.</string> + <string name="notice_room_guest_access_forbidden_by_you">Você impediu que convidados entrem na sala.</string> + + <string name="notice_end_to_end_ok">%1$s ativou a criptografia de ponta a ponta.</string> + <string name="notice_end_to_end_ok_by_you">Você ativou a criptografia de ponta a ponta.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s ativou a criptografia de ponta a ponta (algoritmo não reconhecido %2$s).</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Você ativou a criptografia de ponta a ponta (algoritmo não reconhecido %1$s).</string> + + <string name="key_verification_request_fallback_message">%s deseja verificar a sua chave, mas o seu aplicativo não suporta a verificação da chave da conversa. Você precisará usar a verificação tradicional de chaves para verificar chaves.</string> + + <string name="call_notification_answer">Aceitar</string> + <string name="call_notification_reject">Recusar</string> + <string name="call_notification_hangup">Encerrar</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-pt/strings.xml b/matrix-sdk-android/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..4bc90cf0cb7a1ad4a32d17948bb68da460763b75 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt/strings.xml @@ -0,0 +1,87 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s enviou uma imagem.</string> + + <string name="notice_room_invite_no_invitee">convite de %s</string> + <string name="notice_room_invite">%1$s convidou %2$s</string> + <string name="notice_room_invite_you">%1$s convidou-o</string> + <string name="notice_room_join">%1$s entrou</string> + <string name="notice_room_leave">%1$s saiu</string> + <string name="notice_room_reject">%1$s recusou o convite</string> + <string name="notice_room_kick">%1$s expulsou %2$s</string> + <string name="notice_room_unban">%1$s des-baniu %2$s</string> + <string name="notice_room_ban">%1$s baniu %2$s</string> + <string name="notice_room_withdraw">%1$s cancelou o convite de %2$s</string> + <string name="notice_avatar_url_changed">%1$s mudou o seu avatar</string> + <string name="notice_display_name_set">%1$s definiu seu nome público como %2$s</string> + <string name="notice_display_name_changed_from">%1$s alterou seu nome público de %2$s para %3$s</string> + <string name="notice_display_name_removed">%1$s apagou o seu nome público (%2$s)</string> + <string name="notice_room_topic_changed">%1$s alterou o tópico desta sala para: %2$s</string> + <string name="notice_room_name_changed">%1$s alterou o nome desta sala para: %2$s</string> + <string name="notice_placed_video_call">%s iniciou uma chamada de vÃdeo.</string> + <string name="notice_placed_voice_call">%s iniciou uma chamada de voz.</string> + <string name="notice_answered_call">%s respondeu à chamada.</string> + <string name="notice_ended_call">%s terminou a chamada.</string> + <string name="notice_made_future_room_visibility">%1$s tornou o histórico futuro desta sala visÃvel para %2$s</string> + <string name="notice_room_visibility_invited">todas os membros que integram esta sala, a partir do momento em que foram convidados.</string> + <string name="notice_room_visibility_joined">todas os membros da sala, a partir do momento em que entraram.</string> + <string name="notice_room_visibility_shared">todas os membros da sala.</string> + <string name="notice_room_visibility_world_readable">todos.</string> + <string name="notice_room_visibility_unknown">desconhecida (%s).</string> + <string name="notice_end_to_end">%1$s ativou a criptografia ponta-a-ponta (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s solicitou uma conferência VoIP</string> + <string name="notice_voip_started">A conferência VoIP começou</string> + <string name="notice_voip_finished">A conferência VoIP terminou</string> + + <string name="notice_avatar_changed_too">(o avatar também foi alterado)</string> + <string name="notice_room_name_removed">%1$s removeu o nome da sala</string> + <string name="notice_room_topic_removed">%1$s removeu o tópico da sala</string> + <string name="notice_profile_change_redacted">%1$s atualizou o seu perfil %2$s</string> + <string name="notice_room_third_party_invite">%1$s enviou um convite para que %2$s se junte à sala</string> + <string name="notice_room_third_party_registered_invite">%1$s aceitou o convite para %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** ImpossÃvel decifrar: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">O dispositivo de quem enviou a mensagem não nos enviou as chaves para esta mensagem.</string> + + <!-- Room Screen --> + <string name="could_not_redact">Não foi possÃvel apagar</string> + <string name="unable_to_send_message">Não foi possÃvel enviar a mensagem</string> + + <string name="message_failed_to_upload">O envio da imagem falhou</string> + + <!-- general errors --> + <string name="network_error">Erro de conexão à Internet</string> + <string name="matrix_error">Erro do Matrix</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Ainda não é possÃvel voltar a entrar numa sala vazia.</string> + + <string name="encrypted_message">Mensagem cifrada</string> + + <!-- medium friendly name --> + <string name="medium_email">Endereço de e-mail</string> + <string name="medium_phone_number">Número de telefone</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Convite de %s</string> + <string name="room_displayname_room_invite">Convite para sala</string> + <string name="room_displayname_two_members">%1$s e %2$s</string> + <string name="room_displayname_empty_room">Sala vazia</string> + + + <string name="summary_user_sent_sticker">%1$s enviou um sticker.</string> + + <string name="notice_room_update">%s fez o upgrade da sala.</string> + + <string name="notice_event_redacted">Mensagem removida</string> + <string name="notice_event_redacted_by">Mensagem removida por %1$s</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1657d80f1c1369a3370c5c37e513974827768681 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -0,0 +1,320 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s отправил(а) изображение.</string> + + <string name="notice_room_invite_no_invitee">%s приглашение</string> + <string name="notice_room_invite">%1$s приглаÑил(а) %2$s</string> + <string name="notice_room_invite_you">%1$s приглаÑил(а) ваÑ</string> + <string name="notice_room_join">%1$s вошёл(ла) в комнату</string> + <string name="notice_room_leave">%1$s покинул(а) комнату</string> + <string name="notice_room_reject">%1$s отклонил(а) приглашение</string> + <string name="notice_room_kick">%1$s выгнан %2$s</string> + <string name="notice_room_unban">%1$s разблокировал(а) %2$s</string> + <string name="notice_room_ban">%1$s заблокировал(а) %2$s</string> + <string name="notice_room_withdraw">%1$s отозвал(а) приглашение %2$s</string> + <string name="notice_avatar_url_changed">%1$s изменил(а) Ñвой аватар</string> + <string name="notice_display_name_set">%1$s уÑтановил(а) Ð¸Ð¼Ñ %2$s</string> + <string name="notice_display_name_changed_from">%1$s изменил(а) Ð¸Ð¼Ñ Ñ %2$s на %3$s</string> + <string name="notice_display_name_removed">%1$s удалил(а) Ñвое Ð¸Ð¼Ñ (%2$s)</string> + <string name="notice_room_topic_changed">%1$s изменил(а) тему на: %2$s</string> + <string name="notice_room_name_changed">%1$s изменил(а) название комнаты: %2$s</string> + <string name="notice_placed_video_call">%s начал(а) видеовызов.</string> + <string name="notice_placed_voice_call">%s начал(а) голоÑовой вызов.</string> + <string name="notice_answered_call">%s ответил(а) на звонок.</string> + <string name="notice_ended_call">%s завершил(а) вызов.</string> + <string name="notice_made_future_room_visibility">%1$s Ñделал(а) будущую иÑторию комнаты видимой %2$s</string> + <string name="notice_room_visibility_invited">вÑем членам, Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° их приглашениÑ.</string> + <string name="notice_room_visibility_joined">вÑем членам, Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° приÑоединениÑ.</string> + <string name="notice_room_visibility_shared">вÑем членам.</string> + <string name="notice_room_visibility_world_readable">вÑем.</string> + <string name="notice_room_visibility_unknown">неизвеÑтно (%s).</string> + <string name="notice_end_to_end">%1$s включил(а) Ñквозное шифрование (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s запроÑил(а) VoIP конференцию</string> + <string name="notice_voip_started">VoIP-ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ð¸Ñ Ð½Ð°Ñ‡Ð°Ñ‚Ð°</string> + <string name="notice_voip_finished">VoIP-ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ð¸Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð°</string> + + <string name="notice_avatar_changed_too">(аватар также был изменен)</string> + <string name="notice_room_name_removed">%1$s удалил(а) название комнаты</string> + <string name="notice_room_topic_removed">%1$s удалил(а) тему комнаты</string> + <string name="notice_profile_change_redacted">%1$s обновил(а) Ñвой профиль %2$s</string> + <string name="notice_room_third_party_invite">%1$s отправил(а) приглашение %2$s приÑоединитьÑÑ Ðº комнате</string> + <string name="notice_room_third_party_registered_invite">%1$s принÑл(а) приглашение от %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Ðевозможно раÑшифровать: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">УÑтройÑтво Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð¸Ñ‚ÐµÐ»Ñ Ð½Ðµ предоÑтавило нам ключ Ð´Ð»Ñ Ñ€Ð°Ñшифровки Ñтого ÑообщениÑ.</string> + + <!-- Room Screen --> + <string name="could_not_redact">Ðе удалоÑÑŒ изменить</string> + <string name="unable_to_send_message">Ðе удалоÑÑŒ отправить Ñообщение</string> + + <string name="message_failed_to_upload">Ðе удалоÑÑŒ загрузить изображение</string> + + <!-- general errors --> + <string name="network_error">Ð¡ÐµÑ‚ÐµÐ²Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ°</string> + <string name="matrix_error">Ошибка Matrix</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½ÐµÐ²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾ вновь приÑоединитьÑÑ Ðº пуÑтой комнате.</string> + + <string name="encrypted_message">Зашифрованное Ñообщение</string> + + <!-- medium friendly name --> + <string name="medium_email">ÐÐ´Ñ€ÐµÑ Ñлектронной почты</string> + <string name="medium_phone_number">Ðомер телефона</string> + + <string name="summary_user_sent_sticker">%1$s отправил Ñтикер.</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Приглашение от %s</string> + <string name="room_displayname_room_invite">Приглашение в комнату</string> + <string name="room_displayname_two_members">%1$s и %2$s</string> + <string name="room_displayname_empty_room">ПуÑÑ‚Ð°Ñ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ð°</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s и 1 другой</item> + <item quantity="few">%1$s и %2$d другие</item> + <item quantity="many">%1$s и %2$d других</item> + <item quantity="other" /> + </plurals> + + + <string name="notice_event_redacted">Сообщение удалено</string> + <string name="notice_event_redacted_by">%1$s удалил(а) Ñообщение</string> + <string name="notice_event_redacted_with_reason">Сообщение удалено [причина: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">%1$s удалил(а) Ñообщение [причина: %2$s]</string> + <string name="verification_emoji_dog">Собака</string> + <string name="verification_emoji_cat">Кошка</string> + <string name="verification_emoji_lion">Лев</string> + <string name="verification_emoji_horse">Лошадь</string> + <string name="verification_emoji_unicorn">Единорог</string> + <string name="verification_emoji_pig">ПороÑёнок</string> + <string name="verification_emoji_elephant">Слон</string> + <string name="verification_emoji_rabbit">Кролик</string> + <string name="verification_emoji_panda">Панда</string> + <string name="verification_emoji_rooster">Петух</string> + <string name="verification_emoji_penguin">Пингвин</string> + <string name="verification_emoji_turtle">Черепаха</string> + <string name="verification_emoji_fish">Рыба</string> + <string name="verification_emoji_octopus">ОÑьминог</string> + <string name="verification_emoji_butterfly">Бабочка</string> + <string name="verification_emoji_flower">Цветок</string> + <string name="verification_emoji_tree">Дерево</string> + <string name="verification_emoji_cactus">КактуÑ</string> + <string name="verification_emoji_mushroom">Гриб</string> + <string name="verification_emoji_globe">ЗемлÑ</string> + <string name="verification_emoji_moon">Луна</string> + <string name="verification_emoji_cloud">Облако</string> + <string name="verification_emoji_fire">Огонь</string> + <string name="verification_emoji_banana">Банан</string> + <string name="verification_emoji_apple">Яблоко</string> + <string name="verification_emoji_strawberry">Клубника</string> + <string name="verification_emoji_corn">Кукуруза</string> + <string name="verification_emoji_pizza">Пицца</string> + <string name="verification_emoji_cake">Пирожное</string> + <string name="verification_emoji_heart">Сердце</string> + <string name="verification_emoji_smiley">Смайлик</string> + <string name="verification_emoji_robot">Робот</string> + <string name="verification_emoji_hat">ШлÑпа</string> + <string name="verification_emoji_glasses">Очки</string> + <string name="verification_emoji_wrench">Гаечный ключ</string> + <string name="verification_emoji_santa">Санта</string> + <string name="verification_emoji_thumbsup">Большой палец вверх</string> + <string name="verification_emoji_umbrella">Зонтик</string> + <string name="verification_emoji_hourglass">ПеÑочные чаÑÑ‹</string> + <string name="verification_emoji_clock">ЧаÑÑ‹</string> + <string name="verification_emoji_gift">Подарок</string> + <string name="verification_emoji_lightbulb">Лампочка</string> + <string name="verification_emoji_book">Книга</string> + <string name="verification_emoji_pencil">Карандаш</string> + <string name="verification_emoji_paperclip">Скрепка Ð´Ð»Ñ Ð±ÑƒÐ¼Ð°Ð³</string> + <string name="verification_emoji_scissors">Ðожницы</string> + <string name="verification_emoji_lock">Замок</string> + <string name="verification_emoji_key">Ключ</string> + <string name="verification_emoji_hammer">Молоток</string> + <string name="verification_emoji_telephone">Телефон</string> + <string name="verification_emoji_flag">Флаг</string> + <string name="verification_emoji_train">Поезд</string> + <string name="verification_emoji_bicycle">ВелоÑипед</string> + <string name="verification_emoji_airplane">Самолёт</string> + <string name="verification_emoji_rocket">Ракета</string> + <string name="verification_emoji_trophy">Трофей</string> + <string name="verification_emoji_ball">ÐœÑч</string> + <string name="verification_emoji_guitar">Гитара</string> + <string name="verification_emoji_trumpet">Труба</string> + <string name="verification_emoji_bell">Колокол</string> + <string name="verification_emoji_anchor">Якорь</string> + <string name="verification_emoji_headphone">Ðаушники</string> + <string name="verification_emoji_folder">Папка</string> + <string name="verification_emoji_pin">Булавка</string> + + <string name="initial_sync_start_importing_account">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт учетной запиÑи…</string> + <string name="initial_sync_start_importing_account_crypto">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт криптографии</string> + <string name="initial_sync_start_importing_account_rooms">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт комнат</string> + <string name="initial_sync_start_importing_account_joined_rooms">Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð½Ð°Ñ‡Ð°Ñ‚Ð°: +\nИмпорт приÑоединенных комнат</string> + <string name="initial_sync_start_importing_account_invited_rooms">Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð½Ð°Ñ‡Ð°Ñ‚Ð°: +\nИмпорт приглашенных комнат</string> + <string name="initial_sync_start_importing_account_left_rooms">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт покинутых комнат</string> + <string name="initial_sync_start_importing_account_groups">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт ÑообщеÑтв</string> + <string name="initial_sync_start_importing_account_data">ÐÐ°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑинхронизациÑ: +\nИмпорт данных учетной запиÑи</string> + + <string name="notice_room_update">%s обновил Ñту комнату.</string> + + <string name="event_status_sending_message">Отправка ÑообщениÑ…</string> + <string name="clear_timeline_send_queue">ОчиÑтить очередь отправки</string> + + <string name="notice_room_third_party_revoked_invite">%1$s отозвал приглашение %2$s приÑоединитьÑÑ Ðº комнате</string> + <string name="notice_room_invite_no_invitee_with_reason">Приглашение %1$s. Причина: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s приглашен %2$s. Причина: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s приглаÑил ваÑ. Причина: %2$s</string> + <string name="notice_room_join_with_reason">%1$s вошёл(ла) в комнату. Причина: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s покинул(а) комнату. Причина: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s отклонил приглашение. Причина: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s выгнали %2$s. Причина: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s разблокировано %2$s. Причина: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s забанен %2$s. Причина: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s отправил приглашение %2$s в комнату. Причина: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s отозвал приглашение %2$s приÑоединитьÑÑ Ðº комнате. Причина: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s принÑл приглашение Ð´Ð»Ñ %2$s. Причина: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s отозвал приглашение %2$s. Причина: %3$s</string> + + <string name="notice_room_created">%1$s Ñоздал(а) комнату</string> + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s добавил(а) %2$s в качеÑтве адреÑа Ð´Ð»Ñ Ñтой комнаты.</item> + <item quantity="few">%1$s добавил(а) %2$s в качеÑтве адреÑов Ð´Ð»Ñ Ñтой комнаты.</item> + <item quantity="many">%1$s добавил(а) %2$s в качеÑтве адреÑов Ð´Ð»Ñ Ñтой комнаты.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s удалил(а) Ð°Ð´Ñ€ÐµÑ %2$s Ð´Ð»Ñ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ñ‹.</item> + <item quantity="few">%1$s удалил(а) адреÑа %2$s Ð´Ð»Ñ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ñ‹.</item> + <item quantity="many">%1$s удалил(а) адреÑа %2$s Ð´Ð»Ñ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ñ‹.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s добавил(а) адреÑа %2$s и удалил(а) %3$s Ð´Ð»Ñ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ñ‹.</string> + + <string name="notice_room_canonical_alias_set">%1$s Ñделал(а) %2$s главным адреÑом комнаты.</string> + <string name="notice_room_canonical_alias_unset">%1$s удалил(а) главный Ð°Ð´Ñ€ÐµÑ ÐºÐ¾Ð¼Ð½Ð°Ñ‚Ñ‹.</string> + + <string name="notice_room_guest_access_can_join">%1$s разрешил(а) гоÑÑ‚Ñм входить в комнату.</string> + <string name="notice_room_guest_access_forbidden">%1$s запретил(а) гоÑÑ‚Ñм входить в комнату.</string> + + <string name="notice_end_to_end_ok">%1$s включил(а) Ñквозное шифрование.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s включил(а) Ñквозное шифрование (неизвеÑтный алгоритм %2$s).</string> + + <string name="key_verification_request_fallback_message">%s запрашивает подтверждение вашего ключа, но ваш клиент не поддерживает подтверждение в чате. ИÑпользуйте уÑтаревшую проверку Ð´Ð»Ñ Ñверки ключей.</string> + + <string name="summary_you_sent_image">Ð’Ñ‹ отправили изображение.</string> + <string name="summary_you_sent_sticker">Ð’Ñ‹ отправили Ñтикер.</string> + + <string name="notice_room_invite_no_invitee_by_you">Ваше приглашение</string> + <string name="notice_room_created_by_you">Ð’Ñ‹ Ñоздали комнату</string> + <string name="notice_room_invite_by_you">Ð’Ñ‹ приглаÑили %1$s</string> + <string name="notice_room_join_by_you">Ð’Ñ‹ вошли в комнату</string> + <string name="notice_room_leave_by_you">Ð’Ñ‹ покинули комнату</string> + <string name="notice_room_reject_by_you">Ð’Ñ‹ отклонили приглашение</string> + <string name="notice_room_kick_by_you">Ð’Ñ‹ выгнали %1$s</string> + <string name="notice_room_unban_by_you">Ð’Ñ‹ разбанили %1$s</string> + <string name="notice_room_ban_by_you">Ð’Ñ‹ забанили %1$s</string> + <string name="notice_room_withdraw_by_you">Ð’Ñ‹ отозвали приглашение %1$s</string> + <string name="notice_avatar_url_changed_by_you">Ð’Ñ‹ Ñменили Ñвой аватар</string> + <string name="notice_display_name_set_by_you">Ð’Ñ‹ Ñменили Ñвоё отображаемое Ð¸Ð¼Ñ Ð½Ð° %1$s</string> + <string name="notice_display_name_changed_from_by_you">Ð’Ñ‹ Ñменили Ñвоё отображаемое Ð¸Ð¼Ñ Ñ %1$s на %2$s</string> + <string name="notice_display_name_removed_by_you">Ð’Ñ‹ удалили Ñвоё отображаемое Ð¸Ð¼Ñ (%1$s)</string> + <string name="notice_room_topic_changed_by_you">Ð’Ñ‹ Ñменили тему на: %1$s</string> + <string name="notice_room_name_changed_by_you">Ð’Ñ‹ Ñменили название комнаты на: %1$s</string> + <string name="notice_placed_video_call_by_you">Ð’Ñ‹ начали видеозвонок.</string> + <string name="notice_placed_voice_call_by_you">Ð’Ñ‹ начали звонок.</string> + <string name="notice_answered_call_by_you">Ð’Ñ‹ ответили на звонок.</string> + <string name="notice_ended_call_by_you">Ð’Ñ‹ закончили звонок.</string> + <string name="notice_made_future_room_visibility_by_you">Ð’Ñ‹ Ñделали будущую иÑторию комнаты видимой Ð´Ð»Ñ %1$s</string> + <string name="notice_end_to_end_by_you">Ð’Ñ‹ включили Ñквозное шифрование (%1$s)</string> + <string name="notice_room_update_by_you">Ð’Ñ‹ обновили Ñту комнату.</string> + + <string name="notice_requested_voip_conference_by_you">Ð’Ñ‹ начали групповой звонок</string> + <string name="notice_room_name_removed_by_you">Ð’Ñ‹ удалили название комнаты</string> + <string name="notice_room_topic_removed_by_you">Ð’Ñ‹ удалили тему комнаты</string> + <string name="notice_profile_change_redacted_by_you">Ð’Ñ‹ обновили Ñвой профиль %1$s</string> + <string name="notice_room_third_party_invite_by_you">Ð’Ñ‹ отправили %1$s приглашение в Ñту комнату</string> + <string name="notice_room_third_party_revoked_invite_by_you">Ð’Ñ‹ отозвали у %1$s приглашение в Ñту комнату</string> + <string name="notice_room_third_party_registered_invite_by_you">Ð’Ñ‹ принÑли приглашение Ð´Ð»Ñ %1$s</string> + + <string name="notice_widget_added">%1$s добавил(а) виджет %2$s</string> + <string name="notice_widget_added_by_you">Ð’Ñ‹ добавили виджет %1$s</string> + <string name="notice_widget_removed">%1$s удалил(а) виджет %2$s</string> + <string name="notice_widget_removed_by_you">Ð’Ñ‹ удалили виджет %1$s</string> + <string name="notice_widget_modified">%1$s изменил(а) виджет %2$s</string> + <string name="notice_widget_modified_by_you">Ð’Ñ‹ изменили виджет %1$s</string> + + <string name="power_level_admin">ÐдминиÑтратор</string> + <string name="power_level_moderator">Модератор</string> + <string name="power_level_default">По умолчанию</string> + <string name="power_level_custom">ПользовательÑкий (%1$d)</string> + <string name="power_level_custom_no_value">ПользовательÑкий</string> + + <string name="notice_power_level_changed_by_you">Ð’Ñ‹ изменили уровни доÑтупа %1$s.</string> + <string name="notice_power_level_changed">%1$s изменил(а) уровни доÑтупа %2$s.</string> + <string name="notice_power_level_diff">%1$s Ñ %2$s на %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">Ваше приглашение. Причина: %1$s</string> + <string name="notice_room_invite_with_reason_by_you">Ð’Ñ‹ приглаÑили %1$s. Причина: %2$s</string> + <string name="notice_room_join_with_reason_by_you">Ð’Ñ‹ вошли в комнату. Причина: %1$s</string> + <string name="notice_room_leave_with_reason_by_you">Ð’Ñ‹ покинули комнату. Причина: %1$s</string> + <string name="notice_room_reject_with_reason_by_you">Ð’Ñ‹ отклонили приглашение. Причина: %1$s</string> + <string name="notice_room_kick_with_reason_by_you">Ð’Ñ‹ выгнали %1$s. Причина: %2$s</string> + <string name="notice_room_unban_with_reason_by_you">Ð’Ñ‹ разбанили %1$s. Причина: %2$s</string> + <string name="notice_room_ban_with_reason_by_you">Ð’Ñ‹ забанили %1$s. Причина: %2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Ð’Ñ‹ отправили %1$s приглашение в Ñту комнату. Причина: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Ð’Ñ‹ отозвали у %1$s приглашение в Ñту комнату. Причина: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Ð’Ñ‹ принÑли приглашение Ð´Ð»Ñ %1$s. Причина: %2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">Ð’Ñ‹ отозвали приглашение %1$s. Причина: %2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">Ð’Ñ‹ добавили Ð°Ð´Ñ€ÐµÑ %1$s Ð´Ð»Ñ Ñтой комнаты.</item> + <item quantity="few">Ð’Ñ‹ добавили %1$s в качеÑтве адреÑов Ð´Ð»Ñ Ñтой комнаты.</item> + <item quantity="many">Ð’Ñ‹ добавили %1$s в качеÑтве адреÑов Ð´Ð»Ñ Ñтой комнаты.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">Ð’Ñ‹ удалили Ð°Ð´Ñ€ÐµÑ Ñтой комнаты: %1$s.</item> + <item quantity="few">Ð’Ñ‹ удалили адреÑа Ñтой комнаты: %1$s.</item> + <item quantity="many">Ð’Ñ‹ удалили адреÑа Ñтой комнаты: %1$s.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">Ð’Ñ‹ добавили адреÑа %1$s и удалили %2$s Ð´Ð»Ñ Ñтой комнаты.</string> + + <string name="notice_room_canonical_alias_set_by_you">Ð’Ñ‹ задали главный Ð°Ð´Ñ€ÐµÑ Ñтой комнаты %1$s.</string> + <string name="notice_room_canonical_alias_unset_by_you">Ð’Ñ‹ удалили главный Ð°Ð´Ñ€ÐµÑ Ñтой комнаты.</string> + + <string name="notice_room_guest_access_can_join_by_you">Ð’Ñ‹ разрешили гоÑÑ‚Ñм входить в комнату.</string> + <string name="notice_room_guest_access_forbidden_by_you">Ð’Ñ‹ запретили гоÑÑ‚Ñм входить в комнату.</string> + + <string name="notice_end_to_end_ok_by_you">Ð’Ñ‹ включили Ñквозное шифрование.</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Ð’Ñ‹ включили Ñквозное шифрование (неизвеÑтный алгоритм %1$s).</string> + + <string name="notice_room_avatar_changed">%1$s изменил(а) аватар комнаты</string> + <string name="notice_room_avatar_changed_by_you">Ð’Ñ‹ изменили аватар комнаты</string> + <string name="notice_call_candidates">%s отправил(а) данные Ð´Ð»Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° звонка.</string> + <string name="notice_call_candidates_by_you">Ð’Ñ‹ отправили данные Ð´Ð»Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° звонка.</string> + <string name="notice_room_avatar_removed">%1$s удалил(а) аватар комнаты</string> + <string name="notice_room_avatar_removed_by_you">Ð’Ñ‹ удалили аватар комнаты</string> + <string name="call_notification_answer">ПринÑÑ‚ÑŒ</string> + <string name="call_notification_reject">Отклонить</string> + <string name="call_notification_hangup">Завершить звонок</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..8aec8fccf99b1b12290241639b8690ec192c5fac --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -0,0 +1,202 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s poslal obrázok.</string> + + <string name="notice_room_invite_no_invitee">Pozvanie %s</string> + <string name="notice_room_invite">%1$s pozval %2$s</string> + <string name="notice_room_invite_you">%1$s vás pozval</string> + <string name="notice_room_join">%1$s sa pripojil/a do miestnosti</string> + <string name="notice_room_leave">%1$s opustil/a miestnosÅ¥</string> + <string name="notice_room_reject">%1$s odmietol pozvanie</string> + <string name="notice_room_kick">%1$s vykázal %2$s</string> + <string name="notice_room_unban">%1$s povolil vstup %2$s</string> + <string name="notice_room_ban">%1$s zakázal vstup %2$s</string> + <string name="notice_room_withdraw">%1$s stiahol pozvanie %2$s</string> + <string name="notice_avatar_url_changed">%1$s si zmenil obrázok v profile</string> + <string name="notice_display_name_set">%1$s si nastavil zobrazované meno %2$s</string> + <string name="notice_display_name_changed_from">%1$s si zmenil zobrazované meno z %2$s na %3$s</string> + <string name="notice_display_name_removed">%1$s odstránil svoje zobrazované meno (%2$s)</string> + <string name="notice_room_topic_changed">%1$s zmenil tému na: %2$s</string> + <string name="notice_room_name_changed">%1$s zmenil názov miestnosti na: %2$s</string> + <string name="notice_placed_video_call">%s uskutoÄnil video hovor.</string> + <string name="notice_placed_voice_call">%s uskutoÄnil audio hovor.</string> + <string name="notice_answered_call">%s prijal hovor.</string> + <string name="notice_ended_call">%s ukonÄil hovor.</string> + <string name="notice_made_future_room_visibility">%1$s sprÃstupnil budúcu históriu miestnosti %2$s</string> + <string name="notice_room_visibility_invited">pre vÅ¡etkých Älenov, od kedy boli pozvanÃ.</string> + <string name="notice_room_visibility_joined">pre vÅ¡etkých Älenov, od kedy vstúpili.</string> + <string name="notice_room_visibility_shared">pre vÅ¡etkých Älenov.</string> + <string name="notice_room_visibility_world_readable">pre každého.</string> + <string name="notice_room_visibility_unknown">neznámym (%s).</string> + <string name="notice_end_to_end">%1$s povolil E2E Å¡ifrovanie (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s požiadal o VoIP konferenciu</string> + <string name="notice_voip_started">ZaÄala VoIP konferencia</string> + <string name="notice_voip_finished">SkonÄila VoIP konferencia</string> + + <string name="notice_avatar_changed_too">(a tiež obrázok v profile)</string> + <string name="notice_room_name_removed">%1$s odstránil názov miestnosti</string> + <string name="notice_room_topic_removed">%1$s odstránil tému miestnosti</string> + <string name="notice_profile_change_redacted">%1$s aktualizoval svoj profil %2$s</string> + <string name="notice_room_third_party_invite">%1$s pozval %2$s vstúpiÅ¥ do miestnosti</string> + <string name="notice_room_third_party_registered_invite">%1$s prijal pozvanie do %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Nie je možné deÅ¡ifrovaÅ¥: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Zo zariadenia odosieľateľa nebolo možné zÃskaÅ¥ kľúÄe potrebné na deÅ¡ifrovanie tejto správy.</string> + + <string name="could_not_redact">Nie je ožné vymazaÅ¥</string> + <string name="unable_to_send_message">Nie je možné odoslaÅ¥ správu</string> + + <string name="message_failed_to_upload">Nepodarilo sa nahraÅ¥ obrázok</string> + + <string name="network_error">Chyba siete</string> + <string name="matrix_error">Chyba Matrix</string> + + <string name="room_error_join_failed_empty_room">V súÄasnosti nie je možné znovu vstúpiÅ¥ do prázdnej miestnosti.</string> + + <string name="encrypted_message">Å ifrovaná správa</string> + + <string name="medium_email">Emailová adresa</string> + <string name="medium_phone_number">Telefónne ÄÃslo</string> + + <string name="summary_user_sent_sticker">%1$s poslal nálepku.</string> + + <string name="room_displayname_invite_from">Pozvanie od %s</string> + <string name="room_displayname_room_invite">Pozvanie do miestnosti</string> + <string name="room_displayname_two_members">%1$s a %2$s</string> + <string name="room_displayname_empty_room">Prázdna miestnosÅ¥</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s a 1 ÄalÅ¡Ã</item> + <item quantity="few">%1$s a %2$d ÄalÅ¡Ã</item> + <item quantity="many">%1$s a %2$d ÄalÅ¡Ãch</item> + <item quantity="other" /> + </plurals> + + + <string name="notice_room_update">%s aktualizoval túto miestnosÅ¥.</string> + + <string name="notice_event_redacted">Správa odstránená</string> + <string name="notice_event_redacted_by">Správa odstránená použÃvateľom %1$s</string> + <string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Správa odstránená použÃvateľom %1$s [dôvod: %2$s]</string> + <string name="verification_emoji_dog">Hlava psa</string> + <string name="verification_emoji_cat">Hlava maÄky</string> + <string name="verification_emoji_lion">Hlava leva</string> + <string name="verification_emoji_horse">Kôň</string> + <string name="verification_emoji_unicorn">Hlava jednorožca</string> + <string name="verification_emoji_pig">Hlava prasaÅ¥a</string> + <string name="verification_emoji_elephant">Slon</string> + <string name="verification_emoji_rabbit">Hlava zajaca</string> + <string name="verification_emoji_panda">Hlava pandy</string> + <string name="verification_emoji_rooster">Kohút</string> + <string name="verification_emoji_penguin">TuÄniak</string> + <string name="verification_emoji_turtle">KorytnaÄka</string> + <string name="verification_emoji_fish">Ryba</string> + <string name="verification_emoji_octopus">Chobotnica</string> + <string name="verification_emoji_butterfly">Motýľ</string> + <string name="verification_emoji_flower">Tulipán</string> + <string name="verification_emoji_tree">Listnatý strom</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Huba</string> + <string name="verification_emoji_globe">Zemeguľa</string> + <string name="verification_emoji_moon">Polmesiac</string> + <string name="verification_emoji_cloud">Oblak</string> + <string name="verification_emoji_fire">Oheň</string> + <string name="verification_emoji_banana">Banán</string> + <string name="verification_emoji_apple">ÄŒervené jablko</string> + <string name="verification_emoji_strawberry">Jahoda</string> + <string name="verification_emoji_corn">KukuriÄný klas</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Narodeninová torta</string> + <string name="verification_emoji_heart">ÄŒervené</string> + <string name="verification_emoji_smiley">Å keriaca sa tvár</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Cylinder</string> + <string name="verification_emoji_glasses">Okuliare</string> + <string name="verification_emoji_wrench">Francúzsky kľúÄ</string> + <string name="verification_emoji_santa">Santa Claus</string> + <string name="verification_emoji_thumbsup">Palec nahor</string> + <string name="verification_emoji_umbrella">Dáždnik</string> + <string name="verification_emoji_hourglass">Presýpacie hodiny</string> + <string name="verification_emoji_clock">BudÃk</string> + <string name="verification_emoji_gift">Zabalený darÄek</string> + <string name="verification_emoji_lightbulb">Žiarovka</string> + <string name="verification_emoji_book">Zatvorená kniha</string> + <string name="verification_emoji_pencil">Ceruzka</string> + <string name="verification_emoji_paperclip">Sponka na papier</string> + <string name="verification_emoji_scissors">Nožnice</string> + <string name="verification_emoji_lock">Zatvorená zámka</string> + <string name="verification_emoji_key">KľúÄ</string> + <string name="verification_emoji_hammer">Kladivo</string> + <string name="verification_emoji_telephone">Telefón</string> + <string name="verification_emoji_flag">Kockovaná zástava</string> + <string name="verification_emoji_train">RuÅ¡eň</string> + <string name="verification_emoji_bicycle">Bicykel</string> + <string name="verification_emoji_airplane">Lietadlo</string> + <string name="verification_emoji_rocket">Raketa</string> + <string name="verification_emoji_trophy">Trofej</string> + <string name="verification_emoji_ball">Futbal</string> + <string name="verification_emoji_guitar">Gitara</string> + <string name="verification_emoji_trumpet">Trúbka</string> + <string name="verification_emoji_bell">Zvon</string> + <string name="verification_emoji_anchor">Kotva</string> + <string name="verification_emoji_headphone">Slúchadlá</string> + <string name="verification_emoji_folder">Fascikel</string> + <string name="verification_emoji_pin">Å pendlÃk</string> + + <string name="initial_sync_start_importing_account">Úvodná synchronizácia: +\nPrebieha import úÄtu…</string> + <string name="initial_sync_start_importing_account_crypto">Úvodná synchronizácia: +\nPrebieha import Å¡ifrovacÃch kľúÄov</string> + <string name="initial_sync_start_importing_account_rooms">Úvodná synchronizácia: +\nPrebieha import miestnostÃ</string> + <string name="initial_sync_start_importing_account_joined_rooms">Úvodná synchronizácia: +\nPrebieha import miestnostÃ, do ktorých ste vstúpili</string> + <string name="initial_sync_start_importing_account_invited_rooms">Úvodná synchronizácia: +\nPrebieha import pozvánok</string> + <string name="initial_sync_start_importing_account_left_rooms">Úvodná synchronizácia: +\nPrebieha import opustených miestnostÃ</string> + <string name="initial_sync_start_importing_account_groups">Úvodná synchronizácia: +\nPrebieha import komunÃt</string> + <string name="initial_sync_start_importing_account_data">Úvodná synchronizácia: +\nPrebieha import údajov úÄtu</string> + + <string name="event_status_sending_message">Odosielanie správy…</string> + <string name="clear_timeline_send_queue">VymazaÅ¥ správy na odoslanie</string> + + <string name="notice_room_third_party_revoked_invite">%1$s zamietol pozvanie použÃvateľa %2$s vstúpiÅ¥ do miestnosti</string> + <string name="notice_room_invite_no_invitee_with_reason">Pozvanie od použÃvateľa %1$s. Dôvod: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s pozval použÃvateľa %2$s. Dôvod: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s vás pozval. Dôvod: %2$s</string> + <string name="notice_room_join_with_reason">%1$s sa pripojil/a do miestnosti. Dôvod: %2$s</string> + <string name="notice_room_leave_with_reason">PoužÃvateľ %1$s odiÅ¡iel z miestnosti. Dôvod: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s odmietol pozvanie. Dôvod: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s vyhodil použÃvateľa %2$s. Dôvod: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s znovu pridaný použÃvateľom %2$s. Dôvod: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s vyhodil %2$s. Dôvod: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s poslal pozvánku použÃvateľovi %2$s, aby sa pripojil na miestnosÅ¥. Dôvod: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s odvolal/a pozvánku pre použÃvateľa %2$s na pripojenie sa na miestnosÅ¥. Dôvod: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s prijal pozvanie od použÃvateľa %2$s. Dôvod: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s odoprel/a pozvánku použÃvateľa %2$s. Dôvod: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s pridal/a adresu %2$s pre túto miestnosÅ¥.</item> + <item quantity="few">%1$s pridal/a adresy %2$s pre túto miestnosÅ¥.</item> + <item quantity="other">%1$s pridal/a adresy %2$s pre túto miestnosÅ¥.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s odstránil/a adresu %2$s pre túto miestnosÅ¥.</item> + <item quantity="few">%1$s odstránil/a adresy %2$s pre túto miestnosÅ¥.</item> + <item quantity="other">%1$s odstránil/a adresy %2$s pre túto miestnosÅ¥.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s pridal/a adresu %2$s a odstránil/a adresu %3$s pre túto miestnosÅ¥.</string> + + <string name="notice_room_canonical_alias_set">%1$s nastavil/a hlavnú adresu tejto miestnosti na %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s odstránil/a hlavnú adresu pre túto miestnosÅ¥.</string> + + <string name="notice_room_guest_access_can_join">%1$s povolil/a hosÅ¥om///návÅ¡tevnÃkom prÃstup do tejto miestnosti.</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e63e28288fb0835c304d3c13754b83f13b690618 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -0,0 +1,205 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s dërgoi një figurë.</string> + <string name="notice_room_invite">%1$s ftoi %2$s</string> + <string name="notice_room_invite_you">%1$s ju ftoi</string> + <string name="notice_room_join">%1$s hyri në dhomë</string> + <string name="notice_room_leave">%1$s doli nga dhoma</string> + <string name="notice_room_reject">%1$s hodhi tej ftesën</string> + <string name="notice_room_kick">%1$s përzuri %2$s</string> + <string name="notice_room_ban">%1$s dëboi %2$s</string> + <string name="notice_avatar_url_changed">%1$s ndryshoi avatarin e vet</string> + <string name="notice_room_topic_changed">%1$s ndryshoi temën në: %2$s</string> + <string name="notice_room_name_changed">%1$s ndryshoi emrin e dhomës në: %2$s</string> + <string name="notice_placed_video_call">%s bëri një thirrje video.</string> + <string name="notice_placed_voice_call">%s bëri një thirrje zanore.</string> + <string name="notice_answered_call">%s iu përgjigj thirrjes.</string> + <string name="notice_ended_call">%s e përfundoi thirrjen.</string> + <string name="notice_made_future_room_visibility">%1$s e bëri historikun e ardhshëm të dhomës të dukshëm për %2$s</string> + <string name="notice_room_visibility_invited">për krejt anëtarët e dhomës, prej çastit kur janë ftuar.</string> + <string name="notice_room_visibility_joined">për krejt anëtarët e dhomës, prej çastit kur morën pjesë.</string> + <string name="notice_room_visibility_shared">krejt anëtarët e dhomës.</string> + <string name="notice_room_visibility_world_readable">cilido.</string> + <string name="notice_room_visibility_unknown">e panjohur (%s).</string> + <string name="notice_requested_voip_conference">%1$s kërkoi një konferencë VoIP</string> + <string name="notice_voip_started">Konferenca VoIP filloi</string> + <string name="notice_voip_finished">Konferenca VoIP përfundoi</string> + + <string name="notice_avatar_changed_too">(u ndryshua edhe avatari)</string> + <string name="notice_room_name_removed">%1$s hoqi emrin e dhomës</string> + <string name="notice_profile_change_redacted">%1$s përditësoi profilin e tij %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s pranoi ftesën tuaj për %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** S’arrihet të shfshehtëzohet: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string> + + <string name="could_not_redact">S’u redaktua dot</string> + <string name="unable_to_send_message">S’arrihet të dërgohet mesazh</string> + + <string name="message_failed_to_upload">Ngarkimi i figurës dështoi</string> + + <string name="network_error">Gabim rrjeti</string> + <string name="matrix_error">Gabim Matrix</string> + + <string name="room_error_join_failed_empty_room">Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët.</string> + + <string name="encrypted_message">U fshehtëzua mesazhi</string> + + <string name="medium_email">Adresë email</string> + <string name="medium_phone_number">Numër telefoni</string> + + <string name="room_displayname_invite_from">Ftesë nga %s</string> + <string name="room_displayname_room_invite">Ftesë Dhome</string> + + <string name="room_displayname_two_members">%1$s dhe %2$s</string> + + <string name="room_displayname_empty_room">Dhomë e zbrazët</string> + + <string name="summary_user_sent_sticker">%1$s dërgoi një ngjitës.</string> + + <string name="notice_room_invite_no_invitee">Ftesë e %s</string> + <string name="notice_room_unban">%1$s hoqi dëbimin për %2$s</string> + <string name="notice_room_withdraw">%1$s tërhoqi mbrapsht ftesën për %2$s</string> + <string name="notice_display_name_set">%1$s caktoi për veten emër ekrani %2$s</string> + <string name="notice_display_name_changed_from">%1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s</string> + <string name="notice_display_name_removed">%1$s hoqi emrin e tij në ekran (%2$s)</string> + <string name="notice_end_to_end">%1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s)</string> + + <string name="notice_room_topic_removed">%1$s hoqi temën e dhomës</string> + <string name="notice_room_third_party_invite">%1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë</string> + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s dhe 1 tjetër</item> + <item quantity="other">%1$s dhe %2$d të tjerë</item> + </plurals> + + <string name="notice_event_redacted">Mesazhi u hoq</string> + <string name="notice_event_redacted_by">Mesazhi u hoq nga %1$s</string> + <string name="notice_event_redacted_with_reason">Mesazh i hequr [arsye: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mesazh i hequr nga %1$s [arsye: %2$s]</string> + <string name="verification_emoji_dog">Qen</string> + <string name="verification_emoji_cat">Mace</string> + <string name="verification_emoji_lion">Luan</string> + <string name="verification_emoji_horse">Kalë</string> + <string name="verification_emoji_unicorn">Njëbrirësh</string> + <string name="verification_emoji_pig">Derr</string> + <string name="verification_emoji_elephant">Elefant</string> + <string name="verification_emoji_rabbit">Lepur</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Këndes</string> + <string name="verification_emoji_penguin">Pinguin</string> + <string name="verification_emoji_turtle">Breshkë</string> + <string name="verification_emoji_fish">Peshk</string> + <string name="verification_emoji_octopus">Oktapod</string> + <string name="verification_emoji_butterfly">Flutur</string> + <string name="verification_emoji_flower">Lule</string> + <string name="verification_emoji_tree">Pemë</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Kërpudhë</string> + <string name="verification_emoji_globe">Rruzull</string> + <string name="verification_emoji_moon">Hëna</string> + <string name="verification_emoji_cloud">Re</string> + <string name="verification_emoji_fire">Zjarr</string> + <string name="verification_emoji_banana">Banane</string> + <string name="verification_emoji_apple">Mollë</string> + <string name="verification_emoji_strawberry">Luleshtrydhe</string> + <string name="verification_emoji_corn">Misër</string> + <string name="verification_emoji_pizza">Picë</string> + <string name="verification_emoji_cake">Tortë</string> + <string name="verification_emoji_heart">Zemër</string> + <string name="verification_emoji_smiley">Emotikon</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Kapë</string> + <string name="verification_emoji_glasses">Syze</string> + <string name="verification_emoji_wrench">Çelës</string> + <string name="verification_emoji_santa">Babagjyshi i Vitit të Ri</string> + <string name="verification_emoji_umbrella">Ombrellë</string> + <string name="verification_emoji_hourglass">Klepsidër</string> + <string name="verification_emoji_clock">Sahat</string> + <string name="verification_emoji_gift">Dhuratë</string> + <string name="verification_emoji_lightbulb">Llambë</string> + <string name="verification_emoji_book">Libër</string> + <string name="verification_emoji_pencil">Laps</string> + <string name="verification_emoji_paperclip">Kapëse</string> + <string name="verification_emoji_scissors">Gërshërë</string> + <string name="verification_emoji_lock">Dry</string> + <string name="verification_emoji_key">Kyç</string> + <string name="verification_emoji_hammer">Çekiç</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Flamur</string> + <string name="verification_emoji_train">Tren</string> + <string name="verification_emoji_bicycle">Biçikletë</string> + <string name="verification_emoji_airplane">Aeroplan</string> + <string name="verification_emoji_rocket">Raketë</string> + <string name="verification_emoji_trophy">Trofe</string> + <string name="verification_emoji_ball">Top</string> + <string name="verification_emoji_guitar">Kitarë</string> + <string name="verification_emoji_trumpet">Trombë</string> + <string name="verification_emoji_bell">Kambanë</string> + <string name="verification_emoji_anchor">Spirancë</string> + <string name="verification_emoji_headphone">Kufje</string> + <string name="verification_emoji_folder">Dosje</string> + <string name="notice_room_update">%s e përmirësoi këtë dhomë.</string> + + <string name="initial_sync_start_importing_account">Njëkohësimi Fillestar: +\nPo importohet llogaria…</string> + <string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar: +\nPo importohet kriptografi</string> + <string name="initial_sync_start_importing_account_rooms">Njëkohësimi Fillestar: +\nPo importohen Dhoma</string> + <string name="initial_sync_start_importing_account_joined_rooms">Njëkohësimi Fillestar: +\nPo importohen Dhoma Ku Është Bërë Hyrje</string> + <string name="initial_sync_start_importing_account_invited_rooms">Njëkohësimi Fillestar: +\nPo importohen Dhoma Me Ftesë</string> + <string name="initial_sync_start_importing_account_left_rooms">Njëkohësimi Fillestar: +\nPo importohen Dhoma të Braktisura</string> + <string name="initial_sync_start_importing_account_groups">Njëkohësimi Fillestar: +\nPo importohen Bashkësi</string> + <string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar: +\nPo importohet të Dhëna Llogarie</string> + + <string name="event_status_sending_message">Po dërgohet mesazh…</string> + <string name="clear_timeline_send_queue">Spastro radhë pritjeje</string> + + <string name="notice_room_third_party_revoked_invite">%1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma</string> + <string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s ju ftoi. Arsye: %2$s</string> + <string name="notice_room_join_with_reason">%1$s erdhi në dhomë. Arsye: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s doli nga dhoma. Arsye: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s hodhi poshtë ftesën. Arsye: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s përzuri %2$s. Arsye: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s hoqi dëbimin për %2$s. Arsye: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s dëboi %2$s. Arsye: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s dërgoi një ftesë për %2$s për të ardhur në dhomë. Arsye: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s pranoi ftesën për %2$s. Arsye: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s shtoi %2$s si një adresë për këtë dhomë.</item> + <item quantity="other">%1$s shtoi %2$s si adresa për këtë dhomë.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s hoqi %2$s si adresë për këtë dhomë.</item> + <item quantity="other">%1$s hoqi %3$s si adresa për këtë dhomë.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë.</string> + + <string name="notice_room_canonical_alias_set">%1$s caktoi %2$s si adresë kryesore për këtë dhomë.</string> + <string name="notice_room_canonical_alias_unset">%1$s hoqi adresën kryesore për këtë dhomë.</string> + + <string name="notice_room_guest_access_can_join">%1$s ka lejuar vizitorë të marrin pjesë në dhomë.</string> + <string name="notice_room_guest_access_forbidden">%1$s ka penguar vizitorë të marrin pjesë në dhomë.</string> + + <string name="notice_end_to_end_ok">%1$s aktivizoi fshehtëzim skaj-më-skaj.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s).</string> + + <string name="key_verification_request_fallback_message">%s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh.</string> + + <string name="notice_room_created">%1$s krijo dhomën</string> + <string name="verification_emoji_pin">Fiksoje</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-te/strings.xml b/matrix-sdk-android/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..62f58c9e26f4fe90b2a88e8783e065975cc6b5f0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-te/strings.xml @@ -0,0 +1,71 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="notice_room_invite_no_invitee">%s\'s ఆహà±à°µà°¾à°¨à°‚</string> + <string name="notice_room_invite">%1$s ఆహà±à°µà°¾à°¨à°¿à°‚చారౠ%2$s</string> + <string name="notice_room_leave">%1$s వదిలి వెళారà±</string> + <string name="notice_room_reject">%1$s ఆహà±à°µà°¾à°¨à°¾à°¨à±à°¨à°¿ తిరసà±à°•à°°à°¿à°‚చారà±</string> + <string name="notice_room_kick">%1$s తనà±à°¨à°¾à°¡à± %2$s</string> + <string name="notice_room_unban">%1$s నిషేధానà±à°¨à°¿ %2$s</string> + <string name="notice_room_ban">%1$s నిషేధించారౠ%2$s</string> + <string name="notice_room_withdraw">%1$s ఉపసంహరించà±à°•à±à°‚ది %2$s\'s ఆహà±à°µà°¾à°¨à°‚</string> + <string name="notice_avatar_url_changed">%1$s వారి అవతారà±à°¨à± మారà±à°šà°¾à°°à±</string> + <string name="notice_display_name_set">%1$s వారి à°¡à°¿à°¸à±à°ªà±à°²à±‡ పేరà±à°¨à± ని సెటౠచేసారౠ%2$s</string> + <string name="notice_display_name_changed_from">%1$s వారి à°ªà±à°°à°¦à°°à±à°¶à°¨ పేరà±à°¨à± %2$s à°¨à±à°‚à°¡à°¿ %3$s మారà±à°šà°¾à°°à±</string> + <string name="notice_display_name_removed">%1$s వారి à°ªà±à°°à°¦à°°à±à°¶à°¨ పేరà±à°¨à°¿ తీసివేసారౠ(%2$s)</string> + <string name="notice_room_topic_changed">%1$s అంశం మారà±à°šà°¬à°¡à°¿à°‚ది:%2$s</string> + <string name="notice_room_name_changed">%1$s గది పెరౠమారà±à°šà°¬à°¡à°¿à°‚ది %2$s</string> + <string name="notice_placed_video_call">%s à°’à°• వీడియో కాలà±à°¨à°¿ ఉంచింది.</string> + <string name="notice_placed_voice_call">%s వాయిసౠకాలà±à°¨à°¿ ఉంచారà±.</string> + <string name="notice_answered_call">%s కాలà±à°•à°¿ సమాధానం ఇచà±à°šà°¾à°°à±.</string> + <string name="notice_ended_call">%s కాలౠమà±à°—ిసింది.</string> + <string name="notice_made_future_room_visibility">%1$s à°à°µà°¿à°·à±à°¯à°¤à± గది à°šà°°à°¿à°¤à±à°°à°¨à± %2$s à°•à°¿ కనిపించేలా చేసింది</string> + <string name="notice_room_visibility_invited">పాయింటà±à°¨à±à°‚à°¡à°¿, à°…à°¨à±à°¨à°¿ గది à°¸à°à±à°¯à±à°² వారౠఆహà±à°µà°¾à°¨à°¿à°‚చబడà±à°¡à°¾à°°à±.</string> + <string name="notice_room_visibility_joined">పాయింటౠనà±à°‚à°¡à°¿, à°…à°¨à±à°¨à°¿ à°—à°¦à±à°² à°¸à°à±à°¯à±à°² వారౠచేరారà±.</string> + <string name="notice_room_visibility_shared">à°…à°¨à±à°¨à°¿ à°—à°¦à±à°² à°¸à°à±à°¯à±à°²à±.</string> + <string name="notice_room_visibility_world_readable">ఎవరైనా.</string> + <string name="notice_room_visibility_unknown">తెలియని (%s).</string> + <string name="notice_end_to_end">%1$s à°Žà°‚à°¡à±-à°Ÿà±-à°Žà°‚à°¡à± à°Žà°¨à±à°•à±à°°à°¿à°ªà±à°·à°¨à± ఆనౠచెయà±à°¯à°¬à°¡à°¿à°‚ది (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s వి à°“ à°‡ పి సమావేశానà±à°¨à°¿ à°…à°à±à°¯à°°à±à°¥à°¿à°‚చారà±</string> + <string name="notice_voip_started">వి à°“ à°‡ పి సమావేశం à°ªà±à°°à°¾à°°à°‚à°à°®à±†à±–ంది</string> + <string name="notice_voip_finished">వి à°“ à°‡ పి సమావేశం à°®à±à°—ిసింది</string> + + <string name="notice_avatar_changed_too">(అవతారౠమారà±à°šà°¬à°¡à°¿à°‚ది)</string> + <string name="notice_room_name_removed">%1$s గది పేరౠతొలగించబడింది</string> + <string name="notice_room_topic_removed">%1$s గది అంశానà±à°¨à°¿ తీసివేసారà±</string> + <string name="notice_profile_change_redacted">%1$s వారి à°ªà±à°°à±Šà°«à±†à±–లౠనవీకరించబడింది %2$s</string> + <string name="notice_room_third_party_invite">%1$s గదిలో చేరడానికి %2$s కౠఆహà±à°µà°¾à°¨à°¾à°¨à±à°¨à°¿ పంపారà±</string> + <string name="notice_room_third_party_registered_invite">%2$sకోసం %1$s ఆహà±à°µà°¾à°¨à°¾à°¨à±à°¨à°¿ అంగీకరించారà±</string> + + <string name="notice_crypto_unable_to_decrypt">** à°µà±à°¯à°•à±à°¤à±€à°•à°°à°¿à°‚చడానికి సాధà±à°¯à°‚ కాలేదà±: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">à°ˆ సందేశానికి పంపేవారి పరికరం మాకౠకీలనౠపంపలేదà±.</string> + + <string name="could_not_redact">గది à°¸à±à°•à±à°°à±€à°¨à±</string> + <string name="unable_to_send_message">సందేశం పంపడం సాధà±à°¯à°‚ కాలేదà±</string> + + <string name="message_failed_to_upload">à°šà°¿à°¤à±à°°à°¾à°¨à±à°¨à°¿ à°…à°ªà±à°²à±‹à°¡à± చేయడంలో విఫలమైంది</string> + + <string name="network_error">సాధారణ లోపాలà±</string> + <string name="matrix_error">మాటà±à°°à°¿à°•à±à°¸à± లోపం</string> + + <string name="room_error_join_failed_empty_room">మళà±à°²à±€ ఖాళీ గది ని చేరడానికి à°ªà±à°°à°¸à±à°¤à±à°¤à°‚ ఇది సాధà±à°¯à°‚ కాదà±.</string> + + <string name="encrypted_message">à°Žà°¨à±à°•à±à°°à°¿à°ªà±à°Ÿà±†à°¡à± సందేశం</string> + + <string name="medium_email">ఇమెయిలౠచిరà±à°¨à°¾à°®à°¾</string> + <string name="medium_phone_number">ఫోనౠనంబరà±</string> + + + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s à°’à°• à°šà°¿à°¤à±à°°à°‚ పంపారà±.</string> + + <string name="notice_room_invite_you">%1$s మిమà±à°®à°²à±à°¨à°¿ ఆహà±à°µà°¾à°¨à°¿à°‚చారà±</string> + <string name="notice_room_join">%1$s చేరారà±</string> + + <string name="room_displayname_invite_from">%s à°¨à±à°‚à°¡à°¿ ఆహà±à°µà°¾à°¨à°¿à°‚à°šà±</string> + <string name="room_displayname_two_members">%1$s మరియౠ%2$s</string> + <string name="room_displayname_room_invite">గదికి ఆహà±à°µà°¾à°¨à°‚</string> + <string name="room_displayname_empty_room">ఖాళీ గది</string> + + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-th/strings.xml b/matrix-sdk-android/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3abd948f77ebb778569ad4e7c61ddcba9c645ab4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-th/strings.xml @@ -0,0 +1,4 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb5071f1902a01e80c8345b48bff7c6c8bb1f13d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml @@ -0,0 +1,84 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s надіÑлав(ла) зображеннÑ.</string> + + <string name="notice_room_invite_no_invitee">%s запрошеннÑ</string> + <string name="notice_room_invite">%1$s запроÑив(ла) %2$s</string> + <string name="encrypted_message">Зашифроване повідомленнÑ</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Ð—Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð²Ñ–Ð´ %s</string> + <string name="room_displayname_room_invite">Ð—Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð´Ð¾ кімнати</string> + <string name="room_displayname_two_members">%1$s Ñ– %2$s</string> + <string name="room_displayname_empty_room">ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ ÐºÑ–Ð¼Ð½Ð°Ñ‚Ð°</string> + + + <string name="summary_user_sent_sticker">%1$s надіÑлав(ла) наліпку.</string> + + <string name="notice_room_invite_you">%1$s запроÑив(ла) ВаÑ</string> + <string name="notice_room_join">%1$s приєднавÑÑ(лаÑÑŒ)</string> + <string name="notice_room_leave">%1$s покинув(ла)</string> + <string name="notice_room_reject">%1$s відхилив(ла) запрошеннÑ</string> + <string name="notice_room_kick">%1$s копнув(ла) %2$s</string> + <string name="notice_room_unban">%1$s розблокував(ла) %2$s</string> + <string name="notice_room_ban">%1$s заблокував(ла) %2$s</string> + <string name="notice_room_withdraw">%1$s відкликав(ла) Ð·Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð´Ð»Ñ %2$s</string> + <string name="notice_avatar_url_changed">%1$s змінив(ла) Ñвій аватар</string> + <string name="notice_display_name_set">%1$s вÑтановив(ла) Ñобі Ñ–Ð¼â€™Ñ %2$s</string> + <string name="notice_display_name_changed_from">%1$s змінив(ла) Ñвоє Ñ–Ð¼â€™Ñ Ð· %2$s на %3$s</string> + <string name="notice_display_name_removed">%1$s прибрав(ла) Ñвоє Ñ–Ð¼â€™Ñ (%2$s)</string> + <string name="notice_room_topic_changed">%1$s змінив(ла) тему на: %2$s</string> + <string name="notice_room_name_changed">%1$s змінив(ла) назву кімнати на: %2$s</string> + <string name="notice_placed_video_call">%s розпочав(ла) відеодзвінок.</string> + <string name="notice_placed_voice_call">%s розпочав(ла) голоÑовий дзвінок.</string> + <string name="notice_answered_call">%s відповів(ла) на дзвінок.</string> + <string name="notice_ended_call">%s завершив(ла) дзвінок.</string> + <string name="notice_made_future_room_visibility">%1$s зробив(ла) майбутню Ñ–Ñторію кімнати видимою Ð´Ð»Ñ %2$s</string> + <string name="notice_room_visibility_invited">уÑÑ–Ñ… Ñпіврозмовників, з моменту Ñ—Ñ… запрошеннÑ.</string> + <string name="notice_room_visibility_joined">уÑÑ–Ñ… Ñпіврозмовників, з моменту Ñ—Ñ… приєднаннÑ.</string> + <string name="notice_room_visibility_shared">уÑÑ–Ñ… Ñпіврозмовників.</string> + <string name="notice_room_visibility_world_readable">будь-кого.</string> + <string name="notice_room_visibility_unknown">невідомо (%s).</string> + <string name="notice_end_to_end">%1$s увімкнув(ла) наÑкрізне ÑˆÐ¸Ñ„Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s запроÑив(ла) VoIP конференцію</string> + <string name="notice_voip_started">VoIP ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ñ–Ñ Ñ€Ð¾Ð·Ð¿Ð¾Ñ‡Ð°Ð»Ð°ÑÑŒ</string> + <string name="notice_voip_finished">VoIP ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ñ–Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐ¸Ð»Ð°ÑÑŒ</string> + + <string name="notice_avatar_changed_too">(аватар також змінено)</string> + <string name="notice_room_name_removed">%1$s прибрав(ла) назву кімнати</string> + <string name="notice_room_topic_removed">%1$s прибрав(ла) тему кімнати</string> + <string name="notice_profile_change_redacted">%1$s оновив(ла) Ñвій профіль %2$s</string> + <string name="notice_room_third_party_invite">%1$s надіÑлав(ла) Ð·Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ %2$s приєднатиÑÑ Ð´Ð¾ кімнати</string> + <string name="notice_room_third_party_registered_invite">%1$s прийнÑв(ла) Ð·Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ñƒ %2$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Ðеможливо розшифрувати: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">ПриÑтрій відправника не надіÑлав нам ключ Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ повідомленнÑ.</string> + + <string name="could_not_redact">Ðеможливо відредагувати</string> + <string name="unable_to_send_message">Ðе вдалоÑÑ Ð½Ð°Ð´Ñ–Ñлати повідомленнÑ</string> + + <string name="message_failed_to_upload">Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ зображеннÑ</string> + + <string name="network_error">Помилка мережі</string> + <string name="matrix_error">Помилка Matrix</string> + + <string name="room_error_join_failed_empty_room">Ðаразі неможливо переприєднатиÑÑ Ð´Ð¾ порожньої кімнати.</string> + + <string name="medium_email">ÐдреÑа електронної пошти</string> + <string name="medium_phone_number">Ðомер телефону</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s та 1 інший</item> + <item quantity="few">%1$s та %2$d інші</item> + <item quantity="many">%1$s та %2$d інших</item> + <item quantity="other" /> + </plurals> + + <string name="notice_room_update">%s вдоÑконалили цю кімнату.</string> + + <string name="notice_event_redacted">ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾</string> + <string name="notice_event_redacted_by">%1$s видалили повідомленнÑ</string> + <string name="notice_event_redacted_with_reason">ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾ [причина: %1$s]</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-vls/strings.xml b/matrix-sdk-android/src/main/res/values-vls/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c9132ed35a6c07dc01de3f21f337479ff313d98 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml @@ -0,0 +1,169 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s èt e fotootje gesteurd.</string> + <string name="summary_user_sent_sticker">%1$s èt e sticker gesteurd.</string> + + <string name="notice_room_invite_no_invitee">Uutnodigienge van %s</string> + <string name="notice_room_invite">%1$s èt %2$s uutgenodigd</string> + <string name="notice_room_invite_you">%1$s èt joun uitgenodigd</string> + <string name="notice_room_join">%1$s neemt nu deel an ’t gesprek</string> + <string name="notice_room_leave">%1$s èt ’t gesprek verloatn</string> + <string name="notice_room_reject">%1$s èt d’uitnodigienge geweigerd</string> + <string name="notice_room_kick">%1$s èt %2$s uut ’t gesprek verwyderd</string> + <string name="notice_room_unban">%1$s èt %2$s ountbann</string> + <string name="notice_room_ban">%1$s èt %2$s verbann</string> + <string name="notice_room_withdraw">%1$s èt d’uutnodigienge van %2$s ingetrokkn</string> + <string name="notice_avatar_url_changed">%1$s èt zyn/heur avatar angepast</string> + <string name="notice_display_name_set">%1$s èt zyn/heur noame angepast noa %2$s</string> + <string name="notice_display_name_changed_from">%1$s èt zyn/heur noame angepast van %2$s noa %3$s</string> + <string name="notice_display_name_removed">%1$s èt zyn/heur noame verwyderd (%2$s)</string> + <string name="notice_room_topic_changed">%1$s èt ’t ounderwerp veranderd noa: %2$s</string> + <string name="notice_room_name_changed">%1$s èt de gespreksnoame veranderd noa: %2$s</string> + <string name="notice_placed_video_call">%s èt e video-iproep gemakt.</string> + <string name="notice_placed_voice_call">%s èt e sproakiproep gemakt.</string> + <string name="notice_answered_call">%s èt den iproep beantwoord.</string> + <string name="notice_ended_call">%s èt ipgehangn.</string> + <string name="notice_made_future_room_visibility">%1$s èt de toekomstige gespreksgeschiedenisse zichtboar gemakt vo %2$s</string> + <string name="notice_room_visibility_invited">alle deelnemers an ’t gesprek, vanaf ’t punt dan ze zyn uutgenodigd.</string> + <string name="notice_room_visibility_joined">alle deelnemers an ’t gesprek, vanaf ’t punt dan ze zyn toegetreedn.</string> + <string name="notice_room_visibility_shared">alle deelnemers an ’t gesprek.</string> + <string name="notice_room_visibility_world_readable">iedereen.</string> + <string name="notice_room_visibility_unknown">ounbekend (%s).</string> + <string name="notice_end_to_end">%1$s èt eind-tout-eind-versleutelienge angezet (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s èt e VoIP-vergoaderienge angevroagd</string> + <string name="notice_voip_started">VoIP-vergoaderienge begunn</string> + <string name="notice_voip_finished">VoIP-vergoaderienge gestopt</string> + + <string name="notice_avatar_changed_too">(avatar es ook veranderd)</string> + <string name="notice_room_name_removed">%1$s èt de gespreksnoame verwyderd</string> + <string name="notice_room_topic_removed">%1$s èt ’t gespreksounderwerp verwyderd</string> + <string name="notice_event_redacted">Bericht verwyderd</string> + <string name="notice_event_redacted_by">Bericht verwyderd deur %1$s</string> + <string name="notice_event_redacted_with_reason">Bericht verwyderd [reden: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Bericht verwyderd deur %1$s [reden: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s èt zyn/heur profiel %2$s bygewerkt</string> + <string name="notice_room_third_party_invite">%1$s èt een uutnodigienge noa %2$s gesteurd vo ’t gesprek toe te treedn</string> + <string name="notice_room_third_party_registered_invite">%1$s èt d’uutnodigienge vo %2$s anveird</string> + + <string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">’t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string> + + <string name="could_not_redact">Kosteg nie verwyderd wordn</string> + <string name="unable_to_send_message">Kosteg ’t bericht nie verzendn</string> + + <string name="message_failed_to_upload">Iploadn van ’t fotootje es mislukt</string> + + <string name="network_error">Netwerkfout</string> + <string name="matrix_error">Matrix-fout</string> + + <string name="room_error_join_failed_empty_room">’t Es vo de moment nie meuglik van e leeg gesprek were toe te treedn.</string> + + <string name="encrypted_message">Versleuteld bericht</string> + + <string name="medium_email">E-mailadresse</string> + <string name="medium_phone_number">Telefongnumero</string> + + <string name="room_displayname_invite_from">Uutnodigienge van %s</string> + <string name="room_displayname_room_invite">Gespreksuutnodigienge</string> + + <string name="room_displayname_two_members">%1$s en %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s en 1 andere</item> + <item quantity="other">%1$s en %2$d anderen</item> + </plurals> + + <string name="room_displayname_empty_room">Leeg gesprek</string> + + + <string name="verification_emoji_dog">Hound</string> + <string name="verification_emoji_cat">Katte</string> + <string name="verification_emoji_lion">Leeuw</string> + <string name="verification_emoji_horse">Peird</string> + <string name="verification_emoji_unicorn">Eenhoorn</string> + <string name="verification_emoji_pig">Zwyn</string> + <string name="verification_emoji_elephant">Olifant</string> + <string name="verification_emoji_rabbit">Keun</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Hoane</string> + <string name="verification_emoji_penguin">Pinguin</string> + <string name="verification_emoji_turtle">Schildpadde</string> + <string name="verification_emoji_fish">Vis</string> + <string name="verification_emoji_octopus">Octopus</string> + <string name="verification_emoji_butterfly">Beutervlieg</string> + <string name="verification_emoji_flower">Bloem</string> + <string name="verification_emoji_tree">Boom</string> + <string name="verification_emoji_cactus">Cactus</string> + <string name="verification_emoji_mushroom">Paddestoel</string> + <string name="verification_emoji_globe">Eirdbol</string> + <string name="verification_emoji_moon">Moane</string> + <string name="verification_emoji_cloud">Wolk</string> + <string name="verification_emoji_fire">Vier</string> + <string name="verification_emoji_banana">Banoan</string> + <string name="verification_emoji_apple">Appel</string> + <string name="verification_emoji_strawberry">Freize</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_pizza">Pizza</string> + <string name="verification_emoji_cake">Toarte</string> + <string name="verification_emoji_heart">Erte</string> + <string name="verification_emoji_smiley">Smiley</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Hoed</string> + <string name="verification_emoji_glasses">Bril</string> + <string name="verification_emoji_wrench">Moersleutel</string> + <string name="verification_emoji_santa">Kestman</string> + <string name="verification_emoji_thumbsup">Duum omhooge</string> + <string name="verification_emoji_umbrella">Paraplu</string> + <string name="verification_emoji_hourglass">Zandloper</string> + <string name="verification_emoji_clock">Klok</string> + <string name="verification_emoji_gift">Cadeau</string> + <string name="verification_emoji_lightbulb">Gloeilampe</string> + <string name="verification_emoji_book">Boek</string> + <string name="verification_emoji_pencil">Potlood</string> + <string name="verification_emoji_paperclip">Paperclip</string> + <string name="verification_emoji_scissors">Schoar</string> + <string name="verification_emoji_lock">Hangslot</string> + <string name="verification_emoji_key">Sleutel</string> + <string name="verification_emoji_hammer">Oamer</string> + <string name="verification_emoji_telephone">Telefong</string> + <string name="verification_emoji_flag">Vlagge</string> + <string name="verification_emoji_train">Tring</string> + <string name="verification_emoji_bicycle">Veloo</string> + <string name="verification_emoji_airplane">Vlieger</string> + <string name="verification_emoji_rocket">Rakette</string> + <string name="verification_emoji_trophy">Trofee</string> + <string name="verification_emoji_ball">Bolle</string> + <string name="verification_emoji_guitar">Gitoar</string> + <string name="verification_emoji_trumpet">Trompette</string> + <string name="verification_emoji_bell">Belle</string> + <string name="verification_emoji_anchor">Anker</string> + <string name="verification_emoji_headphone">Koptelefong</string> + <string name="verification_emoji_folder">Mappe</string> + <string name="verification_emoji_pin">Pinne</string> + + <string name="initial_sync_start_importing_account">Initiële synchronisoasje: +\nAccount wor geïmporteerd…</string> + <string name="initial_sync_start_importing_account_crypto">Initiële synchronisoasje: +\nCrypto wor geïmporteerd</string> + <string name="initial_sync_start_importing_account_rooms">Initiële synchronisoasje: +\nGesprekkn wordn geïmporteerd</string> + <string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoasje: +\nDeelgenoomn gesprekken wordn geïmporteerd</string> + <string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoasje: +\nUutgenodigde gesprekkn wordn geïmporteerd</string> + <string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoasje: +\nVerloatn gesprekkn wordn geïmporteerd</string> + <string name="initial_sync_start_importing_account_groups">Initiële synchronisoasje: +\nGemeenschappn wordn geïmporteerd</string> + <string name="initial_sync_start_importing_account_data">Initiële synchronisoasje: +\nAccountgegeevns wordn geïmporteerd</string> + + <string name="notice_room_update">%s èt da gesprek hier ipgewoardeerd.</string> + + <string name="event_status_sending_message">Bericht wor verstuurd…</string> + <string name="clear_timeline_send_queue">Uutgoande wachtreeke leegn</string> + + <string name="notice_room_third_party_revoked_invite">%1$s èt d’uutnodigienge vo %2$s vo ’t gesprek toe te treedn ingetrokkn</string> +</resources> diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ef080e8357cdaf6ef6f58c0f72370ec152d44e1d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,296 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_user_sent_image">%1$s å‘é€äº†ä¸€å¼ 图片。</string> + + <string name="notice_room_invite_no_invitee">%s 的邀请</string> + <string name="notice_room_invite">%1$s 邀请了 %2$s</string> + <string name="notice_room_invite_you">%1$s 邀请了您</string> + <string name="notice_room_join">%1$s åŠ å…¥äº†èŠå¤©å®¤</string> + <string name="notice_room_leave">%1$s 离开了èŠå¤©å®¤</string> + <string name="notice_room_reject">%1$s æ‹’ç»äº†é‚€è¯·</string> + <string name="notice_room_kick">%1$s 移除了 %2$s</string> + <string name="notice_room_unban">%1$s 解å°äº† %2$s</string> + <string name="notice_room_ban">%1$s å°ç¦äº† %2$s</string> + <string name="notice_avatar_url_changed">%1$s æ›´æ¢äº†ä»–们的头åƒ</string> + <string name="notice_display_name_set">%1$s 将他们的昵称设置为 %2$s</string> + <string name="notice_display_name_changed_from">%1$s 把他们的昵称从 %2$s 改为 %3$s</string> + <string name="notice_display_name_removed">%1$s 移除了他们的昵称 (%2$s)</string> + <string name="notice_room_topic_changed">%1$s 把主题改为: %2$s</string> + <string name="notice_room_name_changed">%1$s 把èŠå¤©å®¤å称改为: %2$s</string> + <string name="notice_placed_video_call">%s å‘起了一次视频通è¯ã€‚</string> + <string name="notice_placed_voice_call">%s å‘起了一次è¯éŸ³é€šè¯ã€‚</string> + <string name="notice_answered_call">%s 已接å¬é€šè¯ã€‚</string> + <string name="notice_ended_call">%s 已结æŸé€šè¯ã€‚</string> + <string name="notice_room_visibility_invited">所有èŠå¤©å®¤æˆå‘˜ï¼Œä»Žä»–们被邀请开始。</string> + <string name="notice_room_visibility_joined">所有èŠå¤©å®¤æˆå‘˜ï¼Œä»Žä»–ä»¬åŠ å…¥å¼€å§‹ã€‚</string> + <string name="notice_room_visibility_shared">所有èŠå¤©å®¤æˆå‘˜ã€‚</string> + <string name="notice_room_visibility_world_readable">任何人。</string> + <string name="notice_room_visibility_unknown">未知(%s)。</string> + <string name="notice_end_to_end">%1$s å¼€å¯äº†ç«¯åˆ°ç«¯åŠ 密(%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s 请求了一次 VoIP 会议</string> + <string name="notice_voip_started">VoIP 会议已开始</string> + <string name="notice_voip_finished">VoIP 会议已结æŸ</string> + + <string name="notice_avatar_changed_too">(头åƒä¹Ÿè¢«æ›´æ”¹ï¼‰</string> + <string name="notice_room_name_removed">%1$s 移除了èŠå¤©å®¤å称</string> + <string name="notice_room_topic_removed">%1$s 移除了èŠå¤©å®¤ä¸»é¢˜</string> + <string name="notice_crypto_unable_to_decrypt">** æ— æ³•è§£å¯†ï¼š%s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">å‘é€è€…的设备没有å‘我们å‘é€æ¤æ¶ˆæ¯çš„密钥。</string> + + <string name="unable_to_send_message">æ— æ³•å‘é€æ¶ˆæ¯</string> + + <string name="message_failed_to_upload">ä¸Šä¼ å›¾åƒå¤±è´¥</string> + + <string name="network_error">网络错误</string> + <string name="matrix_error">Matrix 错误</string> + + <string name="room_error_join_failed_empty_room">ç›®å‰æ— 法é‡æ–°åŠ 入一个空的èŠå¤©å®¤ã€‚</string> + + <string name="encrypted_message">å·²åŠ å¯†æ¶ˆæ¯</string> + + <string name="medium_email">电å邮箱地å€</string> + <string name="medium_phone_number">手机å·ç </string> + + <string name="notice_room_withdraw">%1$s 撤回了对 %2$s 的邀请</string> + <string name="notice_made_future_room_visibility">%1$s 让未æ¥çš„èŠå¤©å®¤åŽ†å²è®°å½•å¯¹ %2$s å¯è§</string> + <string name="notice_profile_change_redacted">%1$s 更新了他的个人档案 %2$s</string> + <string name="notice_room_third_party_invite">%1$s å‘ %2$s å‘é€äº†åŠ å…¥èŠå¤©å®¤çš„邀请</string> + <string name="notice_room_third_party_registered_invite">%1$s 接å—了 %2$s 的邀请</string> + + <string name="could_not_redact">æ— æ³•æ’¤å›ž</string> + + <string name="summary_message">%1$s:%2$s</string> + <string name="summary_user_sent_sticker">%1$s å‘é€äº†ä¸€å¼ 贴纸。</string> + + <string name="room_displayname_empty_room">空èŠå¤©å®¤</string> + <string name="room_displayname_invite_from">æ¥è‡ª %s 的邀请</string> + <string name="room_displayname_room_invite">èŠå¤©å®¤é‚€è¯·</string> + <string name="room_displayname_two_members">%1$s å’Œ %2$s</string> + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$s 与其他 %2$d ä½</item> + </plurals> + + <string name="notice_event_redacted">消æ¯å·²è¢«ç§»é™¤</string> + <string name="notice_event_redacted_by">消æ¯å·²è¢« %1$s 移除</string> + <string name="notice_event_redacted_with_reason">消æ¯å·²è¢«ç§»é™¤ [åŽŸå› : %1$s]</string> + <string name="notice_event_redacted_by_with_reason">消æ¯å·²è¢« %1$s 移除 [åŽŸå› : %2$s]</string> + <string name="verification_emoji_dog">ç‹—</string> + <string name="verification_emoji_cat">猫</string> + <string name="verification_emoji_lion">ç‹®å</string> + <string name="verification_emoji_horse">马</string> + <string name="verification_emoji_unicorn">独角兽</string> + <string name="verification_emoji_pig">猪</string> + <string name="verification_emoji_elephant">大象</string> + <string name="verification_emoji_rabbit">å…”å</string> + <string name="verification_emoji_panda">熊猫</string> + <string name="verification_emoji_rooster">公鸡</string> + <string name="verification_emoji_penguin">ä¼é¹…</string> + <string name="verification_emoji_turtle">乌龟</string> + <string name="verification_emoji_fish">é±¼</string> + <string name="verification_emoji_octopus">ç« é±¼</string> + <string name="verification_emoji_butterfly">è´è¶</string> + <string name="verification_emoji_flower">花</string> + <string name="verification_emoji_tree">æ ‘</string> + <string name="verification_emoji_cactus">仙人掌</string> + <string name="verification_emoji_mushroom">蘑è‡</string> + <string name="verification_emoji_globe">地çƒ</string> + <string name="verification_emoji_moon">月亮</string> + <string name="verification_emoji_cloud">云</string> + <string name="verification_emoji_fire">ç«</string> + <string name="verification_emoji_banana">香蕉</string> + <string name="verification_emoji_apple">苹果</string> + <string name="verification_emoji_strawberry">è‰èŽ“</string> + <string name="verification_emoji_corn">玉米</string> + <string name="verification_emoji_pizza">披è¨</string> + <string name="verification_emoji_cake">蛋糕</string> + <string name="verification_emoji_heart">心</string> + <string name="verification_emoji_smiley">微笑</string> + <string name="verification_emoji_robot">机器人</string> + <string name="verification_emoji_hat">帽å</string> + <string name="verification_emoji_glasses">眼镜</string> + <string name="verification_emoji_wrench">扳手</string> + <string name="verification_emoji_santa">圣诞è€äºº</string> + <string name="verification_emoji_thumbsup">点赞</string> + <string name="verification_emoji_umbrella">雨伞</string> + <string name="verification_emoji_hourglass">æ²™æ¼</string> + <string name="verification_emoji_clock">é’Ÿ</string> + <string name="verification_emoji_gift">礼物</string> + <string name="verification_emoji_lightbulb">ç¯æ³¡</string> + <string name="verification_emoji_book">书</string> + <string name="verification_emoji_pencil">铅笔</string> + <string name="verification_emoji_paperclip">回形针</string> + <string name="verification_emoji_scissors">剪刀</string> + <string name="verification_emoji_lock">é”</string> + <string name="verification_emoji_key">钥匙</string> + <string name="verification_emoji_hammer">锤å</string> + <string name="verification_emoji_telephone">电è¯</string> + <string name="verification_emoji_flag">æ——å</string> + <string name="verification_emoji_train">ç«è½¦</string> + <string name="verification_emoji_bicycle">自行车</string> + <string name="verification_emoji_airplane">飞机</string> + <string name="verification_emoji_rocket">ç«ç®</string> + <string name="verification_emoji_trophy">奖æ¯</string> + <string name="verification_emoji_ball">çƒ</string> + <string name="verification_emoji_guitar">å‰ä»–</string> + <string name="verification_emoji_trumpet">å–‡å</string> + <string name="verification_emoji_bell">铃铛</string> + <string name="verification_emoji_anchor">锚</string> + <string name="verification_emoji_headphone">耳机</string> + <string name="verification_emoji_folder">文件夹</string> + <string name="initial_sync_start_importing_account">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥è´¦å·â€¦</string> + <string name="initial_sync_start_importing_account_crypto">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥åŠ 密数æ®</string> + <string name="initial_sync_start_importing_account_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_joined_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥å·²åŠ 入的èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_invited_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥å·²é‚€è¯·çš„èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_left_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥å·²ç¦»å¼€çš„èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_groups">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥ç¤¾åŒº</string> + <string name="initial_sync_start_importing_account_data">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨å¯¼å…¥è´¦å·æ•°æ®</string> + + <string name="notice_room_update">%s å‡çº§äº†æ¤èŠå¤©å®¤ã€‚</string> + + <string name="event_status_sending_message">æ£åœ¨å‘é€æ¶ˆæ¯â€¦</string> + <string name="clear_timeline_send_queue">清除æ£åœ¨å‘é€é˜Ÿåˆ—</string> + + <string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s åŠ å…¥èŠå¤©å®¤çš„邀请</string> + <string name="verification_emoji_pin">置顶</string> + + <string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀请。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_invite_with_reason">%1$s 邀请了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s 邀请了您。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_join_with_reason">%1$s åŠ å…¥äº†èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_leave_with_reason">%1$s 离开了èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_reject_with_reason">%1$s 已拒ç»é‚€è¯·ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_unban_with_reason">%1$s 解å°äº† %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_ban_with_reason">%1$s å°ç¦äº† %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s å·²å‘é€é‚€è¯·ç»™ %2$s æ¥åŠ å…¥èŠå¤©å®¤ã€‚ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s 撤销了 %2$s åŠ å…¥èŠå¤©å®¤çš„邀請。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s æŽ¥å— %2$s 的邀請。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s 撤回了对 %2$s 的邀请。ç†ç”±ï¼š%3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="other">%1$s 新增了 %2$s 为æ¤èŠå¤©å®¤çš„地å€ã€‚</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="other">%1$s 移除了æ¤èŠå¤©å®¤çš„ %3$s 地å€ã€‚</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s 为æ¤èŠå¤©å®¤æ–°å¢žäº† %2$s 并移除 %3$s 地å€ã€‚</string> + + <string name="notice_room_canonical_alias_set">%1$s å°†æ¤èŠå¤©å®¤çš„主地å€è®¾ä¸ºäº† %2$s。</string> + <string name="notice_room_canonical_alias_unset">%1$s 为æ¤èŠå¤©å®¤ç§»é™¤äº†ä¸»åœ°å€ã€‚</string> + + <string name="notice_room_guest_access_can_join">%1$s å·²å…è®¸è®¿å®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + <string name="notice_room_guest_access_forbidden">%1$s å·²ç¦æ¢è®¿å®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + + <string name="notice_end_to_end_ok">%1$s 已开å¯ç«¯åˆ°ç«¯åŠ 密。</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s 已开å¯ç«¯åˆ°ç«¯åŠ å¯†ï¼ˆæ— æ³•è¯†åˆ«çš„æ¼”ç®—æ³• %2$s)。</string> + + <string name="key_verification_request_fallback_message">%s æ£åœ¨è¯·æ±‚验è¯æ‚¨çš„密钥,但您的客户端ä¸æ”¯æ´èŠå¤©ä¸å¯†é’¥éªŒè¯ã€‚ 您将必须使用旧版的密钥验è¯æ¥éªŒè¯é‡‘钥。</string> + + <string name="notice_room_created">%1$s 创建了这个èŠå¤©å®¤</string> + <string name="summary_you_sent_image">您å‘é€äº†ä¸€å¼ 图片。</string> + <string name="summary_you_sent_sticker">您å‘é€äº†ä¸€å¼ 贴纸。</string> + + <string name="notice_room_invite_no_invitee_by_you">您的邀请</string> + <string name="notice_room_created_by_you">您创建了这个èŠå¤©å®¤</string> + <string name="notice_room_invite_by_you">您邀请了 %1$s</string> + <string name="notice_room_join_by_you">æ‚¨åŠ å…¥äº†èŠå¤©å®¤</string> + <string name="notice_room_leave_by_you">您离开了èŠå¤©å®¤</string> + <string name="notice_room_reject_by_you">您拒ç»äº†é‚€è¯·</string> + <string name="notice_room_kick_by_you">您移除了 %1$s</string> + <string name="notice_room_unban_by_you">您解å°äº† %1$s</string> + <string name="notice_room_ban_by_you">您å°ç¦äº† %1$s</string> + <string name="notice_room_withdraw_by_you">您撤回了对 %1$s 的邀请</string> + <string name="notice_avatar_url_changed_by_you">您更æ¢äº†æ‚¨çš„头åƒ</string> + <string name="notice_display_name_set_by_you">您将您的昵称设置为 %1$s</string> + <string name="notice_display_name_changed_from_by_you">您将您的昵称从 %1$s 改为 %2$s</string> + <string name="notice_display_name_removed_by_you">您移除了您的昵称 (%1$s)</string> + <string name="notice_room_topic_changed_by_you">您把主题改为:%1$s</string> + <string name="notice_room_avatar_changed">%1$s å˜æ›´äº†èŠå¤©å®¤å¤´åƒ</string> + <string name="notice_room_avatar_changed_by_you">您å˜æ›´äº†èŠå¤©å®¤å¤´åƒ</string> + <string name="notice_room_name_changed_by_you">您把èŠå¤©å®¤å称改为:%1$s</string> + <string name="notice_placed_video_call_by_you">您å‘起了一次视频通è¯ã€‚</string> + <string name="notice_placed_voice_call_by_you">您å‘起了一次è¯éŸ³é€šè¯ã€‚</string> + <string name="notice_call_candidates">%s å‘é€äº†æ•°æ®ä»¥å»ºç«‹é€šè¯ã€‚</string> + <string name="notice_call_candidates_by_you">您å‘é€äº†æ•°æ®ä»¥å»ºç«‹é€šè¯ã€‚</string> + <string name="notice_answered_call_by_you">您接å¬äº†é€šè¯ã€‚</string> + <string name="notice_ended_call_by_you">您结æŸäº†é€šè¯ã€‚</string> + <string name="notice_made_future_room_visibility_by_you">您已让未æ¥çš„èŠå¤©å®¤è®°å½•å¯¹ %1$s å¯è§</string> + <string name="notice_end_to_end_by_you">您开å¯äº†ç«¯åˆ°ç«¯åŠ 密(%1$s)</string> + <string name="notice_room_update_by_you">您å‡çº§äº†æ¤èŠå¤©å®¤ã€‚</string> + + <string name="notice_requested_voip_conference_by_you">您请求了 VoIP 会议</string> + <string name="notice_room_name_removed_by_you">您移除了èŠå¤©å®¤å称</string> + <string name="notice_room_topic_removed_by_you">您移除了èŠå¤©å®¤ä¸»é¢˜</string> + <string name="notice_room_avatar_removed">%1$s 移除了èŠå¤©å®¤å¤´åƒ</string> + <string name="notice_room_avatar_removed_by_you">您移除了èŠå¤©å®¤å¤´åƒ</string> + <string name="notice_profile_change_redacted_by_you">您更新了您的个人档案 %1$s</string> + <string name="notice_room_third_party_invite_by_you">æ‚¨å‘ %1$s å‘é€äº†åŠ å…¥èŠå¤©å®¤çš„邀请</string> + <string name="notice_room_third_party_revoked_invite_by_you">您已撤回了对 %1$s åŠ å…¥èŠå¤©å®¤çš„邀请</string> + <string name="notice_room_third_party_registered_invite_by_you">您接å—了 %1$s 的邀请</string> + + <string name="notice_widget_added">%1$s æ·»åŠ äº† %2$s å°éƒ¨ä»¶</string> + <string name="notice_widget_added_by_you">æ‚¨æ·»åŠ äº† %1$s å°éƒ¨ä»¶</string> + <string name="notice_widget_removed">%1$s 移除了 %2$s å°éƒ¨ä»¶</string> + <string name="notice_widget_removed_by_you">您移除了 %1$s å°éƒ¨ä»¶</string> + <string name="notice_widget_modified">%1$s 修改了 %2$s å°éƒ¨ä»¶</string> + <string name="notice_widget_modified_by_you">您修改了 %1$s å°éƒ¨ä»¶</string> + + <string name="power_level_admin">管ç†å‘˜</string> + <string name="power_level_moderator">å®¡æ ¸å‘˜</string> + <string name="power_level_default">默认</string> + <string name="power_level_custom">自定义(%1$d)</string> + <string name="power_level_custom_no_value">自定义</string> + + <string name="notice_power_level_changed_by_you">您更改了%1$s çš„æƒåŠ›ç‰çº§ã€‚</string> + <string name="notice_power_level_changed">%1$s 更改了 %2$s çš„æƒåŠ›ç‰çº§ã€‚</string> + <string name="notice_power_level_diff">%1$s 从 %2$s 到 %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">您的邀请。ç†ç”±ï¼š%1$s</string> + <string name="notice_room_invite_with_reason_by_you">您邀请了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_join_with_reason_by_you">æ‚¨åŠ å…¥äº†èŠå¤©å®¤ã€‚ç†ç”±ï¼š%1$s</string> + <string name="notice_room_leave_with_reason_by_you">您离开了èŠå¤©å®¤ã€‚ç†ç”±ï¼š%1$s</string> + <string name="notice_room_reject_with_reason_by_you">您拒ç»äº†é‚€è¯·ã€‚ç†ç”±ï¼š%1$s</string> + <string name="notice_room_kick_with_reason_by_you">您踢走了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_unban_with_reason_by_you">您解å°äº† %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_ban_with_reason_by_you">您å°ç¦äº† %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">您已å‘é€é‚€è¯·ç»™ %1$s æ¥åŠ å…¥èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">您撤销了 %1$s åŠ å…¥èŠå¤©å®¤çš„邀请。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">您接å—了 %1$s 的邀请。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">您撤回了 %1$s 的邀请。ç†ç”±ï¼š%2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="other">您新增了 %1$s 为æ¤èŠå¤©å®¤çš„地å€ã€‚</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="other">您移除了æ¤èŠå¤©å®¤çš„ %2$s 地å€ã€‚</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">您为æ¤èŠå¤©å®¤æ–°å¢žäº† %1$s 并移除了 %2$s 地å€ã€‚</string> + + <string name="notice_room_canonical_alias_set_by_you">您将æ¤èŠå¤©å®¤çš„主地å€è®¾ä¸ºäº† %1$s。</string> + <string name="notice_room_canonical_alias_unset_by_you">您移除了æ¤èŠå¤©å®¤çš„主地å€ã€‚</string> + + <string name="notice_room_guest_access_can_join_by_you">您已å…è®¸è®¿å®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + <string name="notice_room_guest_access_forbidden_by_you">您已ç¦æ¢è®¿å®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + + <string name="notice_end_to_end_ok_by_you">您已开å¯ç«¯åˆ°ç«¯åŠ 密。</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">您已开å¯ç«¯åˆ°ç«¯åŠ å¯†ï¼ˆæ— æ³•è¯†åˆ«çš„ç®—æ³• %1$s)。</string> + + <string name="call_notification_answer">接å—</string> + <string name="call_notification_reject">æ‹’ç»</string> + <string name="call_notification_hangup">挂æ–</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ee2662f143488cd301ffdafc0517e38a7ff09242 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,297 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s:%2$s</string> + <string name="summary_user_sent_image">%1$s 傳é€äº†ä¸€å¼µåœ–片。</string> + + <string name="notice_room_invite_no_invitee">%s 的邀請</string> + <string name="notice_room_invite">%1$s 邀請了 %2$s</string> + <string name="notice_room_invite_you">%1$s 邀請您</string> + <string name="notice_room_join">%1$s å·²åŠ å…¥èŠå¤©å®¤</string> + <string name="notice_room_leave">%1$s 已離開èŠå¤©å®¤</string> + <string name="notice_room_reject">%1$s 拒絕邀請</string> + <string name="notice_room_kick">%1$s 踢出 %2$s</string> + <string name="notice_room_unban">%1$s 解除ç¦æ¢ %2$s</string> + <string name="notice_room_ban">%1$s ç¦æ¢ %2$s</string> + <string name="notice_room_withdraw">%1$s æ”¶å›žäº†å° %2$s 的邀請</string> + <string name="notice_avatar_url_changed">%1$s 變更了他們的大é è²¼</string> + <string name="notice_display_name_set">%1$s è¨å®šäº†ä»–們的顯示å稱為 %2$s</string> + <string name="notice_display_name_changed_from">%1$s 變更了他們的顯示å稱從 %2$s 到 %3$s</string> + <string name="notice_display_name_removed">%1$s 移除了他們的顯示å稱 (%2$s)</string> + <string name="notice_room_topic_changed">%1$s 變更主題為:%2$s</string> + <string name="notice_room_name_changed">%1$s 變更房間å稱為:%2$s</string> + <string name="notice_placed_video_call">%s 撥出了視訊通話。</string> + <string name="notice_placed_voice_call">%s 撥出了語音通話。</string> + <string name="notice_answered_call">%s 回覆了通話。</string> + <string name="notice_ended_call">%s çµæŸé€šè©±ã€‚</string> + <string name="notice_made_future_room_visibility">%1$s 讓房間未來å¯è®“ %2$s 看到æ·å²ç´€éŒ„</string> + <string name="notice_room_visibility_invited">所有的房間æˆå“¡ï¼Œå¾žä»–們被邀請的時間開始。</string> + <string name="notice_room_visibility_joined">所有的房間æˆå“¡ï¼Œå¾žä»–å€‘åŠ å…¥çš„æ™‚é–“é–‹å§‹ã€‚</string> + <string name="notice_room_visibility_shared">所有的房間æˆå“¡ã€‚</string> + <string name="notice_room_visibility_world_readable">任何人。</string> + <string name="notice_room_visibility_unknown">未知 (%s)。</string> + <string name="notice_end_to_end">%1$s 開啟了端å°ç«¯åŠ 密 (%2$s)</string> + + <string name="notice_requested_voip_conference">%1$s 請求了 VoIP 會è°é€šè©±</string> + <string name="notice_voip_started">VoIP 會è°é€šè©±å·²é–‹å§‹</string> + <string name="notice_voip_finished">VoIP 會è°é€šè©±å·²çµæŸ</string> + + <string name="notice_avatar_changed_too">(大é 貼也變更了)</string> + <string name="notice_room_name_removed">%1$s 移除了房間å稱</string> + <string name="notice_room_topic_removed">%1$s 移除了房間主題</string> + <string name="notice_profile_change_redacted">%1$s 更新了他們的基本資料 %2$s</string> + <string name="notice_room_third_party_invite">%1$s 傳é€åŠ 入房間的邀請給 %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s æŽ¥å— %2$s 的邀請</string> + + <string name="notice_crypto_unable_to_decrypt">** 無法解密:%s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">傳é€è€…çš„è£ç½®ä¸¦æœªåœ¨æ¤è¨Šæ¯å‚³é€ä»–們的金鑰。</string> + + <string name="could_not_redact">無法編輯</string> + <string name="unable_to_send_message">無法傳é€è¨Šæ¯</string> + + <string name="message_failed_to_upload">上傳圖片失敗</string> + + <string name="network_error">網路錯誤</string> + <string name="matrix_error">Matrix 錯誤</string> + + <string name="room_error_join_failed_empty_room">ç›®å‰ç„¡æ³•é‡æ–°åŠ 入空房間。</string> + + <string name="encrypted_message">å·²åŠ å¯†çš„è¨Šæ¯</string> + + <string name="medium_email">é›»å郵件</string> + <string name="medium_phone_number">電話號碼</string> + + <string name="summary_user_sent_sticker">%1$s 傳é€äº†ä¸€å¼µè²¼åœ–。</string> + + <string name="room_displayname_invite_from">來自%s 的邀請</string> + <string name="room_displayname_room_invite">èŠå¤©å®¤é‚€è«‹</string> + <string name="room_displayname_two_members">%1$s å’Œ %2$s</string> + + <string name="room_displayname_empty_room">空èŠå¤©å®¤</string> + <plurals name="room_displayname_three_and_more_members"> + <item quantity="other">%1$s å’Œ 和其他 %2$d 個人</item> + </plurals> + + + <string name="notice_event_redacted">訊æ¯å·²ç§»é™¤</string> + <string name="notice_event_redacted_by">訊æ¯å·²è¢« %1$s 移除</string> + <string name="notice_event_redacted_with_reason">訊æ¯å·²ç§»é™¤ [ç†ç”±ï¼š%1$s]</string> + <string name="notice_event_redacted_by_with_reason">訊æ¯å·²è¢« %1$s 移除 [ç†ç”±ï¼š%2$s]</string> + <string name="verification_emoji_dog">ç‹—</string> + <string name="verification_emoji_cat">貓</string> + <string name="verification_emoji_lion">ç…</string> + <string name="verification_emoji_horse">馬</string> + <string name="verification_emoji_unicorn">ç¨è§’ç¸</string> + <string name="verification_emoji_pig">豬</string> + <string name="verification_emoji_elephant">象</string> + <string name="verification_emoji_rabbit">å…”</string> + <string name="verification_emoji_panda">貓熊</string> + <string name="verification_emoji_rooster">公雞</string> + <string name="verification_emoji_penguin">ä¼éµ</string> + <string name="verification_emoji_turtle">龜</string> + <string name="verification_emoji_fish">éš</string> + <string name="verification_emoji_octopus">ç« éš</string> + <string name="verification_emoji_butterfly">è¶</string> + <string name="verification_emoji_flower">花</string> + <string name="verification_emoji_tree">樹</string> + <string name="verification_emoji_cactus">仙人掌</string> + <string name="verification_emoji_mushroom">蘑è‡</string> + <string name="verification_emoji_globe">地çƒ</string> + <string name="verification_emoji_moon">月亮</string> + <string name="verification_emoji_cloud">雲</string> + <string name="verification_emoji_fire">ç«</string> + <string name="verification_emoji_banana">香蕉</string> + <string name="verification_emoji_apple">蘋果</string> + <string name="verification_emoji_strawberry">è‰èŽ“</string> + <string name="verification_emoji_corn">玉米</string> + <string name="verification_emoji_pizza">披薩</string> + <string name="verification_emoji_cake">蛋糕</string> + <string name="verification_emoji_heart">心</string> + <string name="verification_emoji_smiley">微笑</string> + <string name="verification_emoji_robot">機器人</string> + <string name="verification_emoji_hat">帽å</string> + <string name="verification_emoji_glasses">眼é¡</string> + <string name="verification_emoji_wrench">扳手</string> + <string name="verification_emoji_santa">è–誕è€äºº</string> + <string name="verification_emoji_thumbsup">讚</string> + <string name="verification_emoji_umbrella">雨傘</string> + <string name="verification_emoji_hourglass">æ²™æ¼</string> + <string name="verification_emoji_clock">時é˜</string> + <string name="verification_emoji_gift">禮物</string> + <string name="verification_emoji_lightbulb">燈泡</string> + <string name="verification_emoji_book">書</string> + <string name="verification_emoji_pencil">鉛ç†</string> + <string name="verification_emoji_paperclip">è¿´ç´‹é‡</string> + <string name="verification_emoji_scissors">剪刀</string> + <string name="verification_emoji_lock">鎖</string> + <string name="verification_emoji_key">鑰匙</string> + <string name="verification_emoji_hammer">鎚å</string> + <string name="verification_emoji_telephone">電話</string> + <string name="verification_emoji_flag">æ——å</string> + <string name="verification_emoji_train">ç«è»Š</string> + <string name="verification_emoji_bicycle">è…³è¸è»Š</string> + <string name="verification_emoji_airplane">飛機</string> + <string name="verification_emoji_rocket">ç«ç®</string> + <string name="verification_emoji_trophy">çŽç›ƒ</string> + <string name="verification_emoji_ball">çƒ</string> + <string name="verification_emoji_guitar">å‰ä»–</string> + <string name="verification_emoji_trumpet">å–‡å</string> + <string name="verification_emoji_bell">鈴</string> + <string name="verification_emoji_anchor">錨</string> + <string name="verification_emoji_headphone">耳機</string> + <string name="verification_emoji_folder">資料夾</string> + <string name="verification_emoji_pin">別é‡</string> + + <string name="initial_sync_start_importing_account">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥å¸³è™Ÿâ€¦â€¦</string> + <string name="initial_sync_start_importing_account_crypto">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥ crypto</string> + <string name="initial_sync_start_importing_account_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_joined_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥å·²åŠ 入的èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_invited_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥å·²é‚€è«‹çš„èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_left_rooms">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥å·²é›¢é–‹çš„èŠå¤©å®¤</string> + <string name="initial_sync_start_importing_account_groups">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥ç¤¾ç¾¤</string> + <string name="initial_sync_start_importing_account_data">åˆå§‹åŒ–åŒæ¥ï¼š +\næ£åœ¨åŒ¯å…¥å¸³è™Ÿè³‡æ–™</string> + + <string name="notice_room_update">%s å·²å‡ç´šæ¤èŠå¤©å®¤ã€‚</string> + + <string name="event_status_sending_message">æ£åœ¨å‚³é€è¨Šæ¯â€¦â€¦</string> + <string name="clear_timeline_send_queue">清除傳é€ä½‡åˆ—</string> + + <string name="notice_room_third_party_revoked_invite">%1$s 撤銷了 %2$s åŠ å…¥èŠå¤©å®¤çš„邀請</string> + <string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀請。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_invite_with_reason">%1$s 邀請了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s 邀請了您。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_join_with_reason">%1$s å·²åŠ å…¥èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_leave_with_reason">%1$s 已離開èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_reject_with_reason">%1$s 已回絕邀請。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_unban_with_reason">%1$s å–消å°éŽ–了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_ban_with_reason">%1$s å°éŽ–了 %2$s。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s 已傳é€é‚€è«‹çµ¦ %2$s ä¾†åŠ å…¥èŠå¤©å®¤ã€‚ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s 撤銷了 %2$s åŠ å…¥èŠå¤©å®¤çš„邀請。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s æŽ¥å— %2$s 的邀請。ç†ç”±ï¼š%3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s æ’¤å›žäº†å° %2$s 的邀請。ç†ç”±ï¼š%3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="other">%1$s 新增了 %2$s 為æ¤èŠå¤©å®¤çš„地å€ã€‚</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="other">%1$s 移除了æ¤èŠå¤©å®¤çš„ %3$s 地å€ã€‚</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s 為æ¤èŠå¤©å®¤æ–°å¢ž %2$s 並移除 %3$s 地å€ã€‚</string> + + <string name="notice_room_canonical_alias_set">%1$s 為æ¤èŠå¤©å®¤è¨å®šäº† %2$s 為主地å€ã€‚</string> + <string name="notice_room_canonical_alias_unset">%1$s 為æ¤èŠå¤©å®¤ç§»é™¤äº†ä¸»è¦åœ°å€ã€‚</string> + + <string name="notice_room_guest_access_can_join">%1$s å·²å…è¨±è¨ªå®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + <string name="notice_room_guest_access_forbidden">%1$s å·²ç¦æ¢è¨ªå®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + + <string name="notice_end_to_end_ok">%1$s å·²é–‹å•Ÿç«¯åˆ°ç«¯åŠ å¯†ã€‚</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s å·²é–‹å•Ÿç«¯åˆ°ç«¯åŠ å¯†ï¼ˆç„¡æ³•è˜åˆ¥çš„演算法 %2$s)。</string> + + <string name="key_verification_request_fallback_message">%s æ£åœ¨è«‹æ±‚é©—è‰æ‚¨çš„金鑰,但您的客戶端ä¸æ”¯æ´èŠå¤©ä¸é‡‘é‘°é©—è‰ã€‚æ‚¨å°‡å¿…é ˆä½¿ç”¨èˆŠç‰ˆçš„é‡‘é‘°é©—è‰ä¾†é©—è‰é‡‘鑰。</string> + + <string name="notice_room_created">%1$s 建立了èŠå¤©å®¤</string> + <string name="summary_you_sent_image">您傳é€äº†åœ–片。</string> + <string name="summary_you_sent_sticker">您傳é€äº†è²¼åœ–。</string> + + <string name="notice_room_invite_no_invitee_by_you">您的邀請</string> + <string name="notice_room_created_by_you">您建立了èŠå¤©å®¤</string> + <string name="notice_room_invite_by_you">您邀請了 %1$s</string> + <string name="notice_room_join_by_you">æ‚¨åŠ å…¥äº†èŠå¤©å®¤</string> + <string name="notice_room_leave_by_you">您離開的èŠå¤©å®¤</string> + <string name="notice_room_reject_by_you">您回絕了邀請</string> + <string name="notice_room_kick_by_you">您踢除了 %1$s</string> + <string name="notice_room_unban_by_you">您å–消å°éŽ–了 %1$s</string> + <string name="notice_room_ban_by_you">您å°éŽ–了 %1$s</string> + <string name="notice_room_withdraw_by_you">您撤銷了 %1$s 的邀請</string> + <string name="notice_avatar_url_changed_by_you">您變更了您的大é è²¼</string> + <string name="notice_display_name_set_by_you">您將您的顯示å稱è¨å®šç‚º %1$s</string> + <string name="notice_display_name_changed_from_by_you">您將您的顯示å稱從 %1$s 變更為 %2$s</string> + <string name="notice_display_name_removed_by_you">您移除了您的顯示å稱(其曾為 %1$s)</string> + <string name="notice_room_topic_changed_by_you">您將主題變更為:%1$s</string> + <string name="notice_room_avatar_changed">%1$s 變更了èŠå¤©å®¤å¤§é è²¼</string> + <string name="notice_room_avatar_changed_by_you">您變更了èŠå¤©å®¤å¤§é è²¼</string> + <string name="notice_room_name_changed_by_you">您將èŠå¤©å®¤å稱變更為:%1$s</string> + <string name="notice_placed_video_call_by_you">您發起了視訊通話。</string> + <string name="notice_placed_voice_call_by_you">您發起了音訊通話。</string> + <string name="notice_call_candidates">%s 傳é€äº†è³‡æ–™ä»¥å»ºç«‹é€šè©±ã€‚</string> + <string name="notice_call_candidates_by_you">您傳é€äº†è³‡æ–™ä»¥å»ºç«‹é€šè©±ã€‚</string> + <string name="notice_answered_call_by_you">您接了通話。</string> + <string name="notice_ended_call_by_you">您çµæŸäº†é€šè©±ã€‚</string> + <string name="notice_made_future_room_visibility_by_you">您已將未來的èŠå¤©å®¤æ·å²è¨å®šç‚ºå° %1$s å¯è¦‹</string> + <string name="notice_end_to_end_by_you">æ‚¨é–‹å•Ÿäº†ç«¯åˆ°ç«¯åŠ å¯† (%1$s)</string> + <string name="notice_room_update_by_you">您å‡ç´šäº†æ¤èŠå¤©å®¤ã€‚</string> + + <string name="notice_requested_voip_conference_by_you">您請求了 VoIP 會è°</string> + <string name="notice_room_name_removed_by_you">您移除了èŠå¤©å®¤å稱</string> + <string name="notice_room_topic_removed_by_you">您移除了èŠå¤©å®¤ä¸»é¡Œ</string> + <string name="notice_room_avatar_removed">%1$s 移除了èŠå¤©å®¤å¤§é è²¼</string> + <string name="notice_room_avatar_removed_by_you">您移除了èŠå¤©å®¤å¤§é è²¼</string> + <string name="notice_profile_change_redacted_by_you">您更新了您的個人檔案 %1$s</string> + <string name="notice_room_third_party_invite_by_you">您傳é€äº†é‚€è«‹çµ¦ %1$s ä»¥åŠ å…¥èŠå¤©å®¤</string> + <string name="notice_room_third_party_revoked_invite_by_you">æ‚¨å·²æ’¤éŠ·å° %1$s åŠ å…¥èŠå¤©å®¤çš„邀請</string> + <string name="notice_room_third_party_registered_invite_by_you">您接å—了 %1$s 的邀請</string> + + <string name="notice_widget_added">%1$s 新增了 %2$s å°å·¥å…·</string> + <string name="notice_widget_added_by_you">您新增了 %1$s å°å·¥å…·</string> + <string name="notice_widget_removed">%1$s 移除了 %2$s å°å·¥å…·</string> + <string name="notice_widget_removed_by_you">您移除了 %1$s å°å·¥å…·</string> + <string name="notice_widget_modified">%1$s 修改了 %2$s å°å·¥å…·</string> + <string name="notice_widget_modified_by_you">您修改了 %1$s å°å·¥å…·</string> + + <string name="power_level_admin">管ç†å“¡</string> + <string name="power_level_moderator">æ¿ä¸»</string> + <string name="power_level_default">é è¨</string> + <string name="power_level_custom">自訂 (%1$d)</string> + <string name="power_level_custom_no_value">自訂</string> + + <string name="notice_power_level_changed_by_you">您變更了 %1$s 的權力ç‰ç´šã€‚</string> + <string name="notice_power_level_changed">%1$s 變更了 %2$s 的權力ç‰ç´šã€‚</string> + <string name="notice_power_level_diff">%1$s 從 %2$s 到 %3$s</string> + + <string name="notice_room_invite_no_invitee_with_reason_by_you">您的邀請。ç†ç”±ï¼š%1$s</string> + <string name="notice_room_invite_with_reason_by_you">您邀請了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_join_with_reason_by_you">æ‚¨åŠ å…¥äº†èŠå¤©å®¤ã€‚ç†ç”±ï¼š%1$s</string> + <string name="notice_room_leave_with_reason_by_you">您離開了èŠå¤©å®¤ã€‚ç†ç”±ï¼š%1$s</string> + <string name="notice_room_reject_with_reason_by_you">您回絕了邀請。ç†ç”±ï¼š%1$s</string> + <string name="notice_room_kick_with_reason_by_you">您踢除了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_unban_with_reason_by_you">您å–消å°éŽ–了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_ban_with_reason_by_you">您å°éŽ–了 %1$s。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">您傳甕了邀請給 %1$s ä»¥åŠ å…¥èŠå¤©å®¤ã€‚ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">您撤銷了 %1$s åŠ å…¥èŠå¤©å®¤çš„邀請。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">您接å—了 %1$s 的邀請。ç†ç”±ï¼š%2$s</string> + <string name="notice_room_withdraw_with_reason_by_you">您撤回了 %1$s 的邀請。ç†ç”±ï¼š%2$s</string> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="other">您為æ¤èŠå¤©å®¤æ–°å¢žäº† %1$s 作為地å€ã€‚</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="other">您為æ¤èŠå¤©å®¤ç§»é™¤äº† %2$s 作為地å€ã€‚</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed_by_you">您為æ¤èŠå¤©å®¤æ–°å¢žäº† %1$s 並移除了 %2$s 作為地å€ã€‚</string> + + <string name="notice_room_canonical_alias_set_by_you">您將æ¤èŠå¤©å®¤çš„主è¦åœ°å€è¨å®šç‚º %1$s。</string> + <string name="notice_room_canonical_alias_unset_by_you">您將æ¤èŠå¤©å®¤çš„主è¦åœ°å€ç§»é™¤ã€‚</string> + + <string name="notice_room_guest_access_can_join_by_you">您已å…è¨±è¨ªå®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + <string name="notice_room_guest_access_forbidden_by_you">您已阻æ¢è¨ªå®¢åŠ å…¥èŠå¤©å®¤ã€‚</string> + + <string name="notice_end_to_end_ok_by_you">æ‚¨é–‹å•Ÿäº†ç«¯åˆ°ç«¯åŠ å¯†ã€‚</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">æ‚¨é–‹å•Ÿäº†ç«¯åˆ°ç«¯åŠ å¯†ï¼ˆç„¡æ³•è˜åˆ¥çš„演算法 %1$s)。</string> + + <string name="call_notification_answer">接å—</string> + <string name="call_notification_reject">拒絕</string> + <string name="call_notification_hangup">掛斷</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0dc64c1b4b9d5f15f4bcdbd1ce0eafaaee900dc6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -0,0 +1,368 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s sent an image.</string> + <string name="summary_you_sent_image">You sent an image.</string> + <string name="summary_user_sent_sticker">%1$s sent a sticker.</string> + <string name="summary_you_sent_sticker">You sent a sticker.</string> + + <string name="notice_room_invite_no_invitee">%s\'s invitation</string> + <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_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_room_leave">%1$s left the room</string> + <string name="notice_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> + <string name="notice_room_kick_by_you">You kicked %1$s</string> + <string name="notice_room_unban">%1$s unbanned %2$s</string> + <string name="notice_room_unban_by_you">You unbanned %1$s</string> + <string name="notice_room_ban">%1$s banned %2$s</string> + <string name="notice_room_ban_by_you">You banned %1$s</string> + <string name="notice_room_withdraw">%1$s withdrew %2$s\'s invitation</string> + <string name="notice_room_withdraw_by_you">You withdrew %1$s\'s invitation</string> + <string name="notice_avatar_url_changed">%1$s changed their avatar</string> + <string name="notice_avatar_url_changed_by_you">You changed your avatar</string> + <string name="notice_display_name_set">%1$s set their display name to %2$s</string> + <string name="notice_display_name_set_by_you">You set your display name to %1$s</string> + <string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string> + <string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string> + <string name="notice_display_name_removed">%1$s removed their display name (it was %2$s)</string> + <string name="notice_display_name_removed_by_you">You removed your display name (it was %1$s)</string> + <string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string> + <string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string> + <string name="notice_room_avatar_changed">%1$s changed the room avatar</string> + <string name="notice_room_avatar_changed_by_you">You changed the room avatar</string> + <string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string> + <string name="notice_room_name_changed_by_you">You changed the room name to: %1$s</string> + <string name="notice_placed_video_call">%s placed a video call.</string> + <string name="notice_placed_video_call_by_you">You placed a video call.</string> + <string name="notice_placed_voice_call">%s placed a voice call.</string> + <string name="notice_placed_voice_call_by_you">You placed a voice call.</string> + <string name="notice_call_candidates">%s sent data to setup the call.</string> + <string name="notice_call_candidates_by_you">You sent data to setup the call.</string> + <string name="notice_answered_call">%s answered the call.</string> + <string name="notice_answered_call_by_you">You answered the call.</string> + <string name="notice_ended_call">%s ended the call.</string> + <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_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> + <string name="notice_room_visibility_world_readable">anyone.</string> + <string name="notice_room_visibility_unknown">unknown (%s).</string> + <string name="notice_end_to_end">%1$s turned on end-to-end encryption (%2$s)</string> + <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_requested_voip_conference">%1$s requested a VoIP conference</string> + <string name="notice_requested_voip_conference_by_you">You requested a VoIP conference</string> + <string name="notice_voip_started">VoIP conference started</string> + <string name="notice_voip_finished">VoIP conference finished</string> + + <string name="notice_avatar_changed_too">(avatar was changed too)</string> + <string name="notice_room_name_removed">%1$s removed the room name</string> + <string name="notice_room_name_removed_by_you">You removed the room name</string> + <string name="notice_room_topic_removed">%1$s removed the room topic</string> + <string name="notice_room_topic_removed_by_you">You removed the room topic</string> + <string name="notice_room_avatar_removed">%1$s removed the room avatar</string> + <string name="notice_room_avatar_removed_by_you">You removed the room avatar</string> + <string name="notice_event_redacted">Message removed</string> + <string name="notice_event_redacted_by">Message removed by %1$s</string> + <string name="notice_event_redacted_with_reason">Message removed [reason: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Message removed by %1$s [reason: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s updated their profile %2$s</string> + <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_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_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> + + <string name="notice_widget_added">%1$s added %2$s widget</string> + <string name="notice_widget_added_by_you">You added %1$s widget</string> + <string name="notice_widget_removed">%1$s removed %2$s widget</string> + <string name="notice_widget_removed_by_you">You removed %1$s widget</string> + <string name="notice_widget_modified">%1$s modified %2$s widget</string> + <string name="notice_widget_modified_by_you">You modified %1$s widget</string> + + <string name="power_level_admin">Admin</string> + <string name="power_level_moderator">Moderator</string> + <string name="power_level_default">Default</string> + <string name="power_level_custom">Custom (%1$d)</string> + <string name="power_level_custom_no_value">Custom</string> + + <!-- parameter will be a comma separated list of values of notice_power_level_diff --> + <string name="notice_power_level_changed_by_you">You changed the power level of %1$s.</string> + <!-- First parameter will be a userId or display name, second one will be a comma separated list of values of notice_power_level_diff --> + <string name="notice_power_level_changed">%1$s changed the power level of %2$s.</string> + <!-- First parameter will be a userId or display name, the two last ones will be value of power_level_* --> + <string name="notice_power_level_diff">%1$s from %2$s to %3$s</string> + + <string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string> + + <!-- Messages --> + + <!-- Room Screen --> + <string name="could_not_redact">Could not redact</string> + <string name="unable_to_send_message">Unable to send message</string> + + <string name="message_failed_to_upload">Failed to upload image</string> + + <!-- general errors --> + <string name="network_error">Network error</string> + <string name="matrix_error">Matrix error</string> + + <!-- Home Screen --> + + <!-- Last seen time --> + + <!-- call events --> + + <!-- room error messages --> + <string name="room_error_join_failed_empty_room">It is not currently possible to re-join an empty room.</string> + + <string name="encrypted_message">Encrypted message</string> + + <!-- medium friendly name --> + <string name="medium_email">Email address</string> + <string name="medium_phone_number">Phone number</string> + + <!-- Room display name --> + <string name="room_displayname_invite_from">Invite from %s</string> + <string name="room_displayname_room_invite">Room Invite</string> + + <!-- The 2 parameters will be members' name --> + <string name="room_displayname_two_members">%1$s and %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s and 1 other</item> + <item quantity="other">%1$s and %2$d others</item> + </plurals> + + <string name="room_displayname_empty_room">Empty room</string> + + + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_dog">Dog</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_cat">Cat</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_lion">Lion</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_horse">Horse</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_unicorn">Unicorn</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_pig">Pig</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_elephant">Elephant</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_rabbit">Rabbit</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_panda">Panda</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_rooster">Rooster</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_penguin">Penguin</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_turtle">Turtle</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_fish">Fish</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_octopus">Octopus</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_butterfly">Butterfly</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_flower">Flower</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_tree">Tree</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_cactus">Cactus</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_mushroom">Mushroom</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_globe">Globe</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_moon">Moon</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_cloud">Cloud</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_fire">Fire</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_banana">Banana</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_apple">Apple</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_strawberry">Strawberry</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_corn">Corn</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_pizza">Pizza</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_cake">Cake</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_heart">Heart</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_smiley">Smiley</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_robot">Robot</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_hat">Hat</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_glasses">Glasses</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_wrench">Wrench</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_santa">Santa</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_thumbsup">Thumbs Up</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_umbrella">Umbrella</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_hourglass">Hourglass</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_clock">Clock</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_gift">Gift</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_lightbulb">Light Bulb</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_book">Book</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_pencil">Pencil</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_paperclip">Paperclip</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_scissors">Scissors</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_lock">Lock</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_key">Key</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_hammer">Hammer</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_telephone">Telephone</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_flag">Flag</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_train">Train</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_bicycle">Bicycle</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_airplane">Airplane</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_rocket">Rocket</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_trophy">Trophy</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_ball">Ball</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_guitar">Guitar</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_trumpet">Trumpet</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_bell">Bell</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_anchor">Anchor</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_headphone">Headphones</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_folder">Folder</string> + <!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb --> + <string name="verification_emoji_pin">Pin</string> + + <!-- Strings for RiotX --> + <string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string> + <string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string> + <string name="initial_sync_start_importing_account_rooms">Initial Sync:\nImporting Rooms</string> + <string name="initial_sync_start_importing_account_joined_rooms">Initial Sync:\nImporting Joined Rooms</string> + <string name="initial_sync_start_importing_account_invited_rooms">Initial Sync:\nImporting Invited Rooms</string> + <string name="initial_sync_start_importing_account_left_rooms">Initial Sync:\nImporting Left Rooms</string> + <string name="initial_sync_start_importing_account_groups">Initial Sync:\nImporting Communities</string> + <string name="initial_sync_start_importing_account_data">Initial Sync:\nImporting Account Data</string> + + <string name="event_status_sending_message">Sending message…</string> + <string name="clear_timeline_send_queue">Clear sending queue</string> + + <string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string> + <string name="notice_room_invite_no_invitee_with_reason_by_you">Your invitation. Reason: %1$s</string> + <string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string> + <string name="notice_room_invite_with_reason_by_you">You invited %1$s. Reason: %2$s</string> + <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_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_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> + <string name="notice_room_kick_with_reason_by_you">You kicked %1$s. Reason: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string> + <string name="notice_room_unban_with_reason_by_you">You unbanned %1$s. Reason: %2$s</string> + <string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string> + <string name="notice_room_ban_with_reason_by_you">You banned %1$s. Reason: %2$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">You sent an invitation to %1$s to join the room. Reason: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">You revoked the invitation for %1$s to join the room. Reason: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">You accepted the invitation for %1$s. Reason: %2$s</string> + <string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string> + <string name="notice_room_withdraw_with_reason_by_you">You withdrew %1$s\'s invitation. Reason: %2$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s added %2$s as an address for this room.</item> + <item quantity="other">%1$s added %2$s as addresses for this room.</item> + </plurals> + + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="one">You added %1$s as an address for this room.</item> + <item quantity="other">You added %1$s as addresses for this room.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s removed %2$s as an address for this room.</item> + <item quantity="other">%1$s removed %3$s as addresses for this room.</item> + </plurals> + + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="one">You removed %1$s as an address for this room.</item> + <item quantity="other">You removed %2$s as addresses for this room.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string> + <string name="notice_room_aliases_added_and_removed_by_you">You added %1$s and removed %2$s as addresses for this room.</string> + + <string name="notice_room_canonical_alias_set">"%1$s set the main address for this room to %2$s."</string> + <string name="notice_room_canonical_alias_set_by_you">"You set the main address for this room to %1$s."</string> + <string name="notice_room_canonical_alias_unset">"%1$s removed the main address for this room."</string> + <string name="notice_room_canonical_alias_unset_by_you">"You removed the main address for this room."</string> + + <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_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_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> + <string name="notice_end_to_end_unknown_algorithm">%1$s turned on end-to-end encryption (unrecognised algorithm %2$s).</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">You turned on end-to-end encryption (unrecognised algorithm %1$s).</string> + + <string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string> + + <string name="call_notification_answer">Accept</string> + <string name="call_notification_reject">Decline</string> + <string name="call_notification_hangup">Hang Up</string> + +</resources> diff --git a/matrix-sdk-android/src/main/res/xml/network_security_config.xml b/matrix-sdk-android/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..e40c61c229c446d7c5ac357c1209d7a29e703401 --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <!-- Ref: https://developer.android.com/training/articles/security-config.html --> + <!-- By default, do not allow clearText traffic --> + <base-config cleartextTrafficPermitted="false" /> + + <!-- Allow clearText traffic on some specified host --> + <domain-config cleartextTrafficPermitted="true"> + <!-- Localhost --> + <domain includeSubdomains="true">localhost</domain> + <domain includeSubdomains="true">127.0.0.1</domain> + <!-- Localhost for Android emulator --> + <domain includeSubdomains="true">10.0.2.2</domain> + </domain-config> + +</network-security-config> diff --git a/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..7c15e41df39aedee95bd4d59a1cc71adb037c3db --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths> + <cache-path + name="downloads" + path="/" /> +</paths> \ No newline at end of file diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4fed36216664ece9ff530fb57f54f0473795cda --- /dev/null +++ b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import okhttp3.Interceptor +import okhttp3.Response +import org.matrix.android.sdk.internal.di.MatrixScope +import java.io.IOException +import javax.inject.Inject + +/** + * No op interceptor + */ +@MatrixScope +internal class CurlLoggingInterceptor @Inject constructor() + : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed(chain.request()) + } +} diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt new file mode 100644 index 0000000000000000000000000000000000000000..69b15a1fa51d2f67a937914fb786a3978f2a239b --- /dev/null +++ b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import androidx.annotation.NonNull +import okhttp3.logging.HttpLoggingInterceptor + +/** + * No op logger + */ +internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { + + @Synchronized + override fun log(@NonNull message: String) { + } +} diff --git a/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt b/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt new file mode 100644 index 0000000000000000000000000000000000000000..52aa7ea0c7e9d7ea18d3e9f8a47ac7821a4b6b88 --- /dev/null +++ b/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.shared + +import net.lachlanmckee.timberjunit.TimberTestRule + +internal fun createTimberTestRule(): TimberTestRule { + return TimberTestRule.builder() + .showThread(false) + .showTimestamp(false) + .onlyLogWhenTestFails(false) + .build() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0933c710634c3091f40348b9cd2b54af985e3b6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk + +import org.matrix.android.sdk.test.shared.createTimberTestRule +import org.junit.Rule + +interface MatrixTest { + + @Rule + fun timberTestRule() = createTimberTestRule() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..69e2f12eb70dd002571d3fa60589bff2f0c5b490 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk +import org.amshove.kluent.shouldBe +import org.junit.Test + +class VersionsKtTest { + + @Test + fun isSupportedBySdkTooLow() { + Versions(supportedVersions = listOf("r0.4.0")).isSupportedBySdk() shouldBe false + Versions(supportedVersions = listOf("r0.4.1")).isSupportedBySdk() shouldBe false + } + + @Test + fun isSupportedBySdkUnstable() { + Versions(supportedVersions = listOf("r0.4.0"), unstableFeatures = mapOf("m.lazy_load_members" to true)).isSupportedBySdk() shouldBe true + } + + @Test + fun isSupportedBySdkOk() { + Versions(supportedVersions = listOf("r0.5.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.1")).isSupportedBySdk() shouldBe true + } + + // Was not working + @Test + fun isSupportedBySdkLater() { + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.1")).isSupportedBySdk() shouldBe true + } + + // Cover cases of issue #1442 + @Test + fun isSupportedBySdk1442() { + Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f213e1b1c1fe6b0bfed17432f7dcef3338f95032 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class PushRuleActionsTest: MatrixTest { + + @Test + fun test_action_parsing() { + val rawPushRule = """ + { + "rule_id": ".m.rule.invite_for_me", + "default": true, + "enabled": true, + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + }, + { + "key": "content.membership", + "kind": "event_match", + "pattern": "invite" + }, + { + "key": "state_key", + "kind": "event_match", + "pattern": "[the user's Matrix ID]" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ] + } + """.trimIndent() + + val pushRule = MoshiProvider.providesMoshi().adapter<PushRule>(PushRule::class.java).fromJson(rawPushRule) + + assertNotNull("Should have parsed the rule", pushRule) + + val actions = pushRule!!.getActions() + assertEquals(3, actions.size) + + assertTrue("First action should be notify", actions[0] is Action.Notify) + + assertTrue("Second action should be sound", actions[1] is Action.Sound) + assertEquals("Second action should have default sound", "default", (actions[1] as Action.Sound).sound) + + assertTrue("Third action should be highlight", actions[2] is Action.Highlight) + assertEquals("Third action tweak param should be false", false, (actions[2] as Action.Highlight).highlight) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..be5aeaaf0f908bdb03911e6edd5a0ed5cc18ca7c --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.Room +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.MessageTextContent +import org.matrix.android.sdk.internal.session.room.RoomGetter +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class PushrulesConditionTest: MatrixTest { + + /* ========================================================================================== + * Test EventMatchCondition + * ========================================================================================== */ + + @Test + fun test_eventmatch_type_condition() { + val condition = EventMatchCondition("type", "m.room.message") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Yo wtf?").toContent(), + originServerTs = 0) + + val rm = RoomMemberContent( + Membership.INVITE, + displayName = "Foo", + avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" + ) + val simpleRoomMemberEvent = Event( + type = "m.room.member", + eventId = "mx0", + stateKey = "@foo:matrix.org", + content = rm.toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + assert(!condition.isSatisfied(simpleRoomMemberEvent)) + } + + @Test + fun test_eventmatch_path_condition() { + val condition = EventMatchCondition("content.msgtype", "m.text") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Yo wtf?").toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + + Event( + type = "m.room.member", + eventId = "mx0", + stateKey = "@foo:matrix.org", + content = RoomMemberContent( + Membership.INVITE, + displayName = "Foo", + avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" + ).toContent(), + originServerTs = 0 + ).apply { + assert(EventMatchCondition("content.membership", "invite").isSatisfied(this)) + } + } + + @Test + fun test_eventmatch_cake_condition() { + val condition = EventMatchCondition("content.body", "cake") + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cake?").toContent(), + originServerTs = 0 + ).apply { + assert(condition.isSatisfied(this)) + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Howwasthecake?").toContent(), + originServerTs = 0 + ).apply { + assert(condition.isSatisfied(this)) + } + } + + @Test + fun test_eventmatch_cakelie_condition() { + val condition = EventMatchCondition("content.body", "cake*lie") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cakeisalie?").toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + } + + @Test + fun test_notice_condition() { + val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.notice", "A").toContent(), + originServerTs = 0, + roomId = "2joined").also { + assertTrue("Notice", conditionEqual.isSatisfied(it)) + } + } + + /* ========================================================================================== + * Test RoomMemberCountCondition + * ========================================================================================== */ + + @Test + fun test_roommember_condition() { + val conditionEqual3 = RoomMemberCountCondition("3") + val conditionEqual3Bis = RoomMemberCountCondition("==3") + val conditionLessThan3 = RoomMemberCountCondition("<3") + + val room2JoinedId = "2joined" + val room3JoinedId = "3joined" + + val roomStub2Joined = mockk<Room> { + every { getNumberOfJoinedMembers() } returns 2 + } + + val roomStub3Joined = mockk<Room> { + every { getNumberOfJoinedMembers() } returns 3 + } + + val roomGetterStub = mockk<RoomGetter> { + every { getRoom(room2JoinedId) } returns roomStub2Joined + every { getRoom(room3JoinedId) } returns roomStub3Joined + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "A").toContent(), + originServerTs = 0, + roomId = room2JoinedId).also { + assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) + assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) + assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "A").toContent(), + originServerTs = 0, + roomId = room3JoinedId).also { + assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) + assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) + assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) + } + } + + /* ========================================================================================== + * Test ContainsDisplayNameCondition + * ========================================================================================== */ + + @Test + fun test_displayName_condition() { + val condition = ContainsDisplayNameCondition() + + val event = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cake benoit?").toContent(), + originServerTs = 0, + roomId = "2joined") + + condition.isSatisfied(event, "how") shouldBe true + condition.isSatisfied(event, "How") shouldBe true + condition.isSatisfied(event, "benoit") shouldBe true + condition.isSatisfied(event, "Benoit") shouldBe true + condition.isSatisfied(event, "cake") shouldBe true + + condition.isSatisfied(event, "ben") shouldBe false + condition.isSatisfied(event, "oit") shouldBe false + condition.isSatisfied(event, "enoi") shouldBe false + condition.isSatisfied(event, "H") shouldBe false + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2d10968b6230b64a69e5f8915aaa8a3a7025cfc --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import org.matrix.android.sdk.MatrixTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class Base58Test: MatrixTest { + + @Test + fun encode() { + // Example comes from https://github.com/keis/base58 + assertEquals("StV1DL6CwTryKyV", base58encode("hello world".toByteArray())) + } + + @Test + fun decode() { + // Example comes from https://github.com/keis/base58 + assertArrayEquals("hello world".toByteArray(), base58decode("StV1DL6CwTryKyV")) + } + + @Test + fun encode_curve25519() { + // Encode a 32 bytes key + assertEquals("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr", + base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray())) + } + + @Test + fun decode_curve25519() { + assertArrayEquals(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(), + base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr")) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b9d38862320b36634cb2f847e80eed0a116610d --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import org.matrix.android.sdk.MatrixTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class RecoveryKeyTest: MatrixTest { + + private val curve25519Key = byteArrayOf( + 0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(), + 0x3C.toByte(), 0x16.toByte(), 0xC1.toByte(), 0x72.toByte(), 0x51.toByte(), 0xB2.toByte(), 0x66.toByte(), 0x45.toByte(), + 0xDF.toByte(), 0x4C.toByte(), 0x2F.toByte(), 0x87.toByte(), 0xEB.toByte(), 0xC0.toByte(), 0x99.toByte(), 0x2A.toByte(), + 0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte()) + + @Test + fun isValidRecoveryKey_valid_true() { + assertTrue(isValidRecoveryKey("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d")) + + // Space should be ignored + assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + + // All whitespace should be ignored + assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4\r\nBXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_null_false() { + assertFalse(isValidRecoveryKey(null)) + } + + @Test + fun isValidRecoveryKey_empty_false() { + assertFalse(isValidRecoveryKey("")) + } + + @Test + fun isValidRecoveryKey_wrong_size_false() { + assertFalse(isValidRecoveryKey("abc")) + } + + @Test + fun isValidRecoveryKey_bad_first_byte_false() { + assertFalse(isValidRecoveryKey("FsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_second_byte_false() { + assertFalse(isValidRecoveryKey("EqTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_parity_false() { + assertFalse(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e")) + } + + @Test + fun computeRecoveryKey_ok() { + assertEquals("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d", computeRecoveryKey(curve25519Key)) + } + + @Test + fun extractCurveKeyFromRecoveryKey_ok() { + assertArrayEquals(curve25519Key, extractCurveKeyFromRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cac2d1cba98c0851b6e8be92858bbd68975f1713 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.internal.util.md5 +import org.junit.Assert.assertEquals +import org.junit.Test + +class HelperTest: MatrixTest { + + @Test + fun testHash_ok() { + assertEquals("e9ee13b1ba2afc0825f4e556114785dd", "alice_15428931567802abf5ba7-d685-4333-af47-d38417ab3724:localhost:8480".md5()) + } + + @Test + fun testHash_size_ok() { + // Any String will have a 32 char hash + for (i in 1..100) { + assertEquals(32, "a".repeat(i).md5().length) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f8fe58b7f11cf8514b0a66ce459dab62aca3250 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.MatrixTest +import org.amshove.kluent.shouldEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class BinaryStringTest: MatrixTest { + + /** + * I want to put bytes to a String, and vice versa + */ + @Test + fun testNominalCase() { + val byteArray = ByteArray(256) + for (i in byteArray.indices) { + byteArray[i] = i.toByte() // Random.nextInt(255).toByte() + } + + val str = byteArray.toString(Charsets.ISO_8859_1) + + str.length shouldEqualTo 256 + + // Ok convert back to bytearray + + val result = str.toByteArray(Charsets.ISO_8859_1) + + result.size shouldEqualTo 256 + + for (i in 0..255) { + result[i] shouldEqualTo i.toByte() + result[i] shouldEqualTo byteArray[i] + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7bef4394177e014e4d2bf5bee857e97799ba7080 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.MatrixTest +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.Executors + +class CoroutineSequencersTest: MatrixTest { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + @Test + fun sequencer_should_run_sequential() { + val sequencer = SemaphoreCoroutineSequencer() + val results = ArrayList<String>() + + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + runBlocking { + jobs.joinAll() + } + assertEquals(3, results.size) + assertEquals(results[0], "#1") + assertEquals(results[1], "#2") + assertEquals(results[2], "#3") + } + + @Test + fun sequencer_should_run_parallel() { + val sequencer1 = SemaphoreCoroutineSequencer() + val sequencer2 = SemaphoreCoroutineSequencer() + val sequencer3 = SemaphoreCoroutineSequencer() + val results = ArrayList<String>() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer1.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer2.post { suspendingMethod("#2") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer3.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + runBlocking { + jobs.joinAll() + } + assertEquals(3, results.size) + } + + @Test + fun sequencer_should_jump_to_next_when_current_job_canceled() { + val sequencer = SemaphoreCoroutineSequencer() + val results = ArrayList<String>() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + val result = sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } + println("Result: $result") + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + // We are canceling the second job + jobs[1].cancel() + runBlocking { + jobs.joinAll() + } + assertEquals(2, results.size) + } + + private suspend fun suspendingMethod(name: String): String { + println("BLOCKING METHOD $name STARTS on ${Thread.currentThread().name}") + delay(1000) + println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}") + return name + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..ade79d3acbcdf34d414b5dec04630eb10e7eed67 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':matrix-sdk-android' diff --git a/tools/import_from_element.sh b/tools/import_from_element.sh new file mode 100755 index 0000000000000000000000000000000000000000..bb61a14027e63e0f82188746c16e1ea3e92c414b --- /dev/null +++ b/tools/import_from_element.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +### This script import SDK code from Element Android + +set -e + +elementAndroidPath="../element-android" + +if [ -d "$elementAndroidPath" ]; then + echo "Importing sdk module from Element Android located at ${elementAndroidPath}" +else + echo "Element Android not found at ${elementAndroidPath}. Can not continue." + exit 1 +fi + +# Check that Element Android is on master branch + +pushd $elementAndroidPath + +elementBranch=`git rev-parse --abbrev-ref HEAD` + +if [ "$elementBranch" != "master" ]; then + read -p "Warning, Element Android is not on master branch but on branch '${elementBranch}' . Continue (y/n)? " -n 1 CONT + echo + if [ "$CONT" != "y" ]; then + exit 0 + fi +fi + +popd + +# matrix SDK + +# Delete existing path +echo "Importing matrix-sdk-android..." +rm -rf ./matrix-sdk-android +cp -r ${elementAndroidPath}/matrix-sdk-android . + +# Add all changes to git +git add -A + +# Build the library +./gradlew clean assembleRelease + +# Success + +echo "Success" +echo +echo "Please check the version name before committing and update the changelog" \ No newline at end of file