diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index c1ab98e85d..c4eccd5b1f 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -23,7 +23,7 @@ body:
- type: textarea
id: result
attributes:
- label: Intended result and actual result
+ label: Outcome
placeholder: Tell us what went wrong
value: |
#### What did you expect?
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
index 5d9cfb3c88..71adce718e 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -10,7 +10,7 @@ body:
id: usecase
attributes:
label: Your use case
- description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
+ description: Please feel welcome to include screenshots or mock ups.
placeholder: Tell us what you would like to do!
value: |
#### What would you like to do?
diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml
index 3ab0017ce2..53b70276c5 100644
--- a/.github/workflows/sanity_test.yml
+++ b/.github/workflows/sanity_test.yml
@@ -1,9 +1,9 @@
name: Sanity Test
on:
- pull_request: { }
- push:
- branches: [ main, develop ]
+ schedule:
+ # At 20:00 every day UTC
+ - cron: '0 20 * * *'
# Enrich gradle.properties for CI/CD
env:
@@ -14,13 +14,15 @@ env:
jobs:
integration-tests:
name: Sanity Tests (Synapse)
- runs-on: ubuntu-latest
+ runs-on: macos-latest
strategy:
fail-fast: false
matrix:
- api-level: [28]
+ api-level: [ 29 ]
steps:
- uses: actions/checkout@v2
+ with:
+ ref: develop
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
@@ -46,11 +48,32 @@ jobs:
python3 -m venv .synapse
source .synapse/bin/activate
pip install synapse matrix-synapse
- curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
- | sed s/127.0.0.1/0.0.0.0/g | bash
+ curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
+ | sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '11'
- name: Run sanity tests on API ${{ matrix.api-level }}
uses: reactivecircus/android-emulator-runner@v2
+ continue-on-error: true # allow pipeline to upload failure results
with:
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
api-level: ${{ matrix.api-level }}
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest
+ emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator.log
+ chmod 777 emulator.log
+ adb logcat >> emulator.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
+ - name: Upload Failing Test Report Log
+ if: failure()
+ uses: actions/upload-artifact@v2
+ with:
+ name: sanity-error-results
+ path: |
+ emulator.log
+ failure_screenshots/
diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml
index 40d5507415..4ecc824424 100644
--- a/.github/workflows/triage-incoming.yml
+++ b/.github/workflows/triage-incoming.yml
@@ -8,7 +8,7 @@ jobs:
automate-project-columns:
runs-on: ubuntu-latest
steps:
- - uses: alex-page/github-project-automation-plus@v0.8.1
+ - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with:
project: Issue triage
column: Incoming
diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml
new file mode 100644
index 0000000000..f910cdf7ea
--- /dev/null
+++ b/.github/workflows/triage-move-labelled.yml
@@ -0,0 +1,124 @@
+name: Move labelled issues to correct boards and columns
+
+on:
+ issues:
+ types: [labeled]
+
+jobs:
+ move_needs_info_issues:
+ name: Move X-Needs-Info issues to Need info on triage board
+ runs-on: ubuntu-latest
+ steps:
+ - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
+ with:
+ action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
+ project-url: "https://github.com/vector-im/element-android/projects/4"
+ column-name: "Need info"
+ label-name: "X-Needs-Info"
+
+ add_priority_design_issues_to_project:
+ name: Move priority X-Needs-Design issues to Design project board
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
+ (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'O-Occasional')) &&
+ (contains(github.event.issue.labels.*.name, 'S-Critical') ||
+ contains(github.event.issue.labels.*.name, 'S-Major') ||
+ contains(github.event.issue.labels.*.name, 'S-Minor'))
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:String!,$contentid:String!) {
+ addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
+ projectNextItem {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PN_kwDOAM0swc0sUA"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ move_spaces_issues:
+ name: Move Spaces issues to Delight project board
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-Spaces') ||
+ contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
+ contains(github.event.issue.labels.*.name, 'A-Subspaces')
+ steps:
+ - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
+ with:
+ action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
+ project-url: "https://github.com/orgs/vector-im/projects/6"
+ column-name: "📥 Inbox"
+ label-name: "A-Spaces"
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_delight2
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:String!,$contentid:String!) {
+ addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
+ projectNextItem {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PN_kwDOAM0swc1HvQ"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ move_voice-message_issues:
+ name: Move A-Voice Messages to Voice message board
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-Voice Messages')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:String!,$contentid:String!) {
+ addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
+ projectNextItem {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PN_kwDOAM0swc2KCw"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ move_threads_issues:
+ name: Move A-Threads to Thread board
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-Threads')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:String!,$contentid:String!) {
+ addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
+ projectNextItem {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PN_kwDOAM0swc0rRA"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-move-unlabelled.yml
new file mode 100644
index 0000000000..94bd049b91
--- /dev/null
+++ b/.github/workflows/triage-move-unlabelled.yml
@@ -0,0 +1,35 @@
+name: Move unlabelled from needs info columns to triaged
+
+on:
+ issues:
+ types: [unlabeled]
+
+jobs:
+ Move_Unabeled_Issue_On_Project_Board:
+ name: Move no longer X-Needs-Info issues to Triaged
+ runs-on: ubuntu-latest
+ if: >
+ ${{
+ !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
+ env:
+ BOARD_NAME: "Issue triage"
+ OWNER: ${{ github.repository_owner }}
+ REPO: ${{ github.event.repository.name }}
+ ISSUE: ${{ github.event.issue.number }}
+ steps:
+ - name: Check if issue is already in "${{ env.BOARD_NAME }}"
+ run: |
+ if curl -i -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql | grep "\b$BOARD_NAME\b"; then
+ echo "Issue is already in Project '$BOARD_NAME', proceeding";
+ echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
+ else
+ echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
+ echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
+ fi
+ - name: Move issue
+ uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ if: ${{ env.ALREADY_IN_BOARD == 'true' }}
+ with:
+ project: Issue triage
+ column: Triaged
+ repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml
deleted file mode 100644
index 64de7951c6..0000000000
--- a/.github/workflows/triage-needs-info.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: Move X-Needs-Info into Need info column in the Issue triage board
-
-on:
- issues:
- types: [labeled]
-
-jobs:
- Move_Labeled_Issue_On_Project_Board:
- runs-on: ubuntu-latest
- steps:
- - uses: konradpabjan/move-labeled-or-milestoned-issue@v2.0
- with:
- action-token: ${{ secrets.GITHUB_TOKEN }}
- project-url: "https://github.com/vector-im/element-android/projects/4"
- column-name: "Need info"
- label-name: "X-Needs-Info"
diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml
new file mode 100644
index 0000000000..018bb8bb55
--- /dev/null
+++ b/.github/workflows/triage-priority-bugs.yml
@@ -0,0 +1,55 @@
+name: Move P1 issues into the P1 column for the App Team and Crypto team
+
+on:
+ issues:
+ types: [labeled, unlabeled]
+
+jobs:
+ p1_issues_to_team_workboard:
+ runs-on: ubuntu-latest
+ if: >
+ (!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
+ !contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
+ !contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
+ !contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
+ !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
+ !contains(github.event.issue.labels.*.name, 'A-Spaces') &&
+ !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
+ !contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
+ (contains(github.event.issue.labels.*.name, 'T-Defect') &&
+ contains(github.event.issue.labels.*.name, 'S-Critical') &&
+ (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
+ contains(github.event.issue.labels.*.name, 'S-Major') &&
+ contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'A11y') &&
+ contains(github.event.issue.labels.*.name, 'O-Frequent'))
+ steps:
+ - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ with:
+ project: Android App Team
+ column: P1
+ repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ P1_issues_to_crypto_team_workboard:
+ runs-on: ubuntu-latest
+ if: >
+ (contains(github.event.issue.labels.*.name, 'A-E2EE') ||
+ contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
+ contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
+ contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
+ contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
+ (contains(github.event.issue.labels.*.name, 'T-Defect') &&
+ contains(github.event.issue.labels.*.name, 'S-Critical') &&
+ (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
+ contains(github.event.issue.labels.*.name, 'S-Major') &&
+ contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'A11y') &&
+ contains(github.event.issue.labels.*.name, 'O-Frequent'))
+ steps:
+ - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
+ with:
+ project: Crypto Team
+ column: Ready
+ repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/CHANGES.md b/CHANGES.md
index 9590adc060..7021877ced 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,34 @@
+Changes in Element v1.3.8 (2021-11-17)
+======================================
+
+Features ✨
+----------
+ - Make notification text spoiler aware ([#3477](https://github.com/vector-im/element-android/issues/3477))
+ - Poll Feature - Create Poll Screen (Disabled for now) ([#4367](https://github.com/vector-im/element-android/issues/4367))
+ - Adds support for images inside message notifications ([#4402](https://github.com/vector-im/element-android/issues/4402))
+
+Bugfixes 🐛
+----------
+ - Render markdown in room list ([#452](https://github.com/vector-im/element-android/issues/452))
+ - Fix incorrect cropping of conversation icons ([#4424](https://github.com/vector-im/element-android/issues/4424))
+ - Fix potential NullPointerException crashes in Room and User account data sources ([#4428](https://github.com/vector-im/element-android/issues/4428))
+ - Unable to establish Olm outbound session from fallback key ([#4446](https://github.com/vector-im/element-android/issues/4446))
+ - Fixes intermittent crash on sign out due to the session being incorrectly recreated whilst being closed ([#4480](https://github.com/vector-im/element-android/issues/4480))
+
+SDK API changes ⚠️
+------------------
+ - Add content scanner API from MSC1453
+ API documentation : https://github.com/matrix-org/matrix-content-scanner#api ([#4392](https://github.com/vector-im/element-android/issues/4392))
+ - Breaking SDK API change to PushRuleListener, the separated callbacks have been merged into one with a data class which includes all the previously separated push information ([#4401](https://github.com/vector-im/element-android/issues/4401))
+
+Other changes
+-------------
+ - Finish migration from RxJava to Flow ([#4219](https://github.com/vector-im/element-android/issues/4219))
+ - Remove redundant text in feature request issue form ([#4257](https://github.com/vector-im/element-android/issues/4257))
+ - Add and improve issue triage workflows ([#4435](https://github.com/vector-im/element-android/issues/4435))
+ - Update issue template to bring in line with element-web ([#4452](https://github.com/vector-im/element-android/issues/4452))
+
+
Changes in Element v1.3.7 (2021-11-04)
======================================
diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
index 4ca6ced8fe..573138bf5c 100644
--- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
+++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
@@ -17,6 +17,7 @@
package im.vector.lib.attachmentviewer
+import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Build
import android.os.Bundle
@@ -141,7 +142,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
- window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ } else {
+ @SuppressLint("WrongConstant")
+ window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ }
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
@@ -347,7 +353,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
- window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ } else {
+ @SuppressLint("WrongConstant")
+ window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ }
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
diff --git a/dependencies.gradle b/dependencies.gradle
index 47090d4732..8cc7b3b260 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,8 +1,8 @@
ext.versions = [
'minSdk' : 21,
- 'compileSdk' : 30,
- 'targetSdk' : 30,
+ 'compileSdk' : 31,
+ 'targetSdk' : 31,
'sourceCompat' : JavaVersion.VERSION_11,
'targetCompat' : JavaVersion.VERSION_11,
]
@@ -11,13 +11,13 @@ def gradle = "7.0.3"
// Ref: https://kotlinlang.org/releases.html
def kotlin = "1.5.31"
def kotlinCoroutines = "1.5.2"
-def dagger = "2.40"
+def dagger = "2.40.1"
def retrofit = "2.9.0"
def arrow = "0.8.2"
def markwon = "4.6.2"
def moshi = "1.12.0"
-def lifecycle = "2.2.0"
-def rxBinding = "3.1.0"
+def lifecycle = "2.4.0"
+def flowBinding = "1.2.0"
def epoxy = "4.6.2"
def mavericks = "2.4.0"
def glide = "4.12.0"
@@ -26,7 +26,7 @@ def jjwt = "0.11.2"
def vanniktechEmoji = "0.8.0"
// Testing
-def mockk = "1.12.0"
+def mockk = "1.12.1"
def espresso = "3.4.0"
def androidxTest = "1.4.0"
@@ -41,22 +41,23 @@ ext.libs = [
jetbrains : [
'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines",
- 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines"
+ 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines",
+ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
'appCompat' : "androidx.appcompat:appcompat:1.3.1",
- 'core' : "androidx.core:core-ktx:1.6.0",
+ 'core' : "androidx.core:core-ktx:1.7.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1",
- 'work' : "androidx.work:work-runtime-ktx:2.6.0",
+ 'work' : "androidx.work:work-runtime-ktx:2.7.0",
'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
'junit' : "androidx.test.ext:junit:1.1.3",
- 'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle",
- 'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle",
- 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1",
+ 'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
+ 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
+ 'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
'datastore' : "androidx.datastore:datastore:1.0.0",
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
@@ -102,7 +103,6 @@ ext.libs = [
'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy",
'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy",
'mavericks' : "com.airbnb.android:mavericks:$mavericks",
- 'mavericksRx' : "com.airbnb.android:mavericks-rxjava2:$mavericks",
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
],
mockk : [
@@ -115,13 +115,13 @@ ext.libs = [
'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer",
'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer",
'progressPieIndicator' : "com.github.piasy:ProgressPieIndicator:$bigImageViewer",
- 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer"
+ 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer",
+ 'flowBinding' : "io.github.reactivecircus.flowbinding:flowbinding-android:$flowBinding",
+ 'flowBindingAppcompat' : "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowBinding",
+ 'flowBindingMaterial' : "io.github.reactivecircus.flowbinding:flowbinding-material:$flowBinding"
],
jakewharton : [
- 'timber' : "com.jakewharton.timber:timber:5.0.1",
- 'rxbinding' : "com.jakewharton.rxbinding3:rxbinding:$rxBinding",
- 'rxbindingAppcompat' : "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBinding",
- 'rxbindingMaterial' : "com.jakewharton.rxbinding3:rxbinding-material:$rxBinding"
+ 'timber' : "com.jakewharton.timber:timber:5.0.1"
],
jsonwebtoken: [
'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt",
diff --git a/docs/rx_flow_migration.md b/docs/rx_flow_migration.md
new file mode 100644
index 0000000000..a438b0f6fb
--- /dev/null
+++ b/docs/rx_flow_migration.md
@@ -0,0 +1,41 @@
+Useful links:
+- https://github.com/ReactiveCircus/FlowBinding
+- https://ivanisidrowu.github.io/kotlin/2020/08/09/Kotlin-Flow-Migration-And-Testing.html
+
+
+Rx is now completely removed from Element dependencies.
+Some examples of the changes:
+
+```
+ sharedActionViewModel
+ .observe()
+ .subscribe { handleQuickActions(it) }
+ .disposeOnDestroyView()
+ ```
+
+became
+
+ ```
+ sharedActionViewModel
+ .stream()
+ .onEach { handleQuickActions(it) }
+ .launchIn(viewLifecycleOwner.lifecycleScope)
+
+```
+
+Inside fragment use
+```
+launchIn(viewLifecycleOwner.lifecycleScope)
+```
+Inside activity use
+```
+launchIn(lifecycleScope)
+```
+Inside viewModel use
+```
+launchIn(viewModelScope)
+```
+
+Also be aware that when using these scopes the coroutine is launched on Dispatchers.Main by default.
+
+
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103050.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103050.txt
new file mode 100644
index 0000000000..6aee9110ef
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Přidání podpory přítomnosti pro místnost s přímými zprávami (poznámka: přítomnost je na matrix.org zakázána). Opět přidána podpora Android Auto.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103060.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103060.txt
new file mode 100644
index 0000000000..ef29377a46
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Přidání podpory přítomnosti pro místnost s přímými zprávami (poznámka: přítomnost je na matrix.org zakázána). Opět přidána podpora Android Auto.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/en-US/changelogs/40103070.txt b/fastlane/metadata/android/en-US/changelogs/40103070.txt
index ad225133c2..9a7030728a 100644
--- a/fastlane/metadata/android/en-US/changelogs/40103070.txt
+++ b/fastlane/metadata/android/en-US/changelogs/40103070.txt
@@ -1,2 +1,2 @@
Main changes in this version: Bug fixes mainly regarding the notifications.
-Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7
\ No newline at end of file
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40103080.txt b/fastlane/metadata/android/en-US/changelogs/40103080.txt
new file mode 100644
index 0000000000..fc00c5da9e
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40103080.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Bug fixes!
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.8
\ No newline at end of file
diff --git a/fastlane/metadata/android/et/changelogs/40103050.txt b/fastlane/metadata/android/et/changelogs/40103050.txt
new file mode 100644
index 0000000000..c68db70b6a
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: Lisasime otsevestlustele kasutaja võrguolekute toe (matrix.org puhul on välja lülitatud) ja uuesti lisasime Android Auto toe.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/et/changelogs/40103060.txt b/fastlane/metadata/android/et/changelogs/40103060.txt
new file mode 100644
index 0000000000..d1c44870fa
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: Lisasime otsevestlustele kasutaja võrguolekute toe (matrix.org puhul on välja lülitatud) ja uuesti lisasime Android Auto toe.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103050.txt b/fastlane/metadata/android/fr-FR/changelogs/40103050.txt
new file mode 100644
index 0000000000..08143ead2f
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : ajout du support pour les indicateurs de présence, dans les conversations privées (attention : les indicateurs de présence sont désactivés sur matrix.org). Réactivation de la prise en charge de Android Auto.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103060.txt b/fastlane/metadata/android/fr-FR/changelogs/40103060.txt
new file mode 100644
index 0000000000..0187c4bf88
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : ajout du support pour les indicateurs de présence, dans les conversations privées (attention : les indicateurs de présence sont désactivés sur matrix.org). Réactivation de la prise en charge de Android Auto.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/fy/short_description.txt b/fastlane/metadata/android/fy/short_description.txt
index ddc559b59c..f0f44ad06c 100644
--- a/fastlane/metadata/android/fy/short_description.txt
+++ b/fastlane/metadata/android/fy/short_description.txt
@@ -1 +1 @@
-Groepsberjochtetsjinst - fersifere berjochten, groeps petearen en fideo skilje
+Groepsberjochtetsjinst - fersifere berjochten, groepspetearen en fideobelje
diff --git a/fastlane/metadata/android/fy/title.txt b/fastlane/metadata/android/fy/title.txt
index c4b5b596fc..0c77d7d613 100644
--- a/fastlane/metadata/android/fy/title.txt
+++ b/fastlane/metadata/android/fy/title.txt
@@ -1 +1 @@
-Element - Feilige Berjochtetsjinst
+Element - Feilige berjochtetsjinst
diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103050.txt b/fastlane/metadata/android/hu-HU/changelogs/40103050.txt
new file mode 100644
index 0000000000..e46bf39f83
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Fő változás ebben a verzióban: Állapot állítási lehetőség közvetlen beszélgetéseknél (megj.: a matrix.org-on az állapot jelzés ki van kapcsolva). Újra elérhető az Android Auto.
+Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103060.txt b/fastlane/metadata/android/hu-HU/changelogs/40103060.txt
new file mode 100644
index 0000000000..1fc6ce5b8a
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Fő változás ebben a verzióban: Állapot állítási lehetőség közvetlen beszélgetéseknél (megj.: a matrix.org-on az állapot jelzés ki van kapcsolva). Újra elérhető az Android Auto.
+Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/id/changelogs/40103040.txt b/fastlane/metadata/android/id/changelogs/40103040.txt
index 60e2e3a4de..0641f72ffd 100644
--- a/fastlane/metadata/android/id/changelogs/40103040.txt
+++ b/fastlane/metadata/android/id/changelogs/40103040.txt
@@ -1,2 +1,2 @@
-Perubahan utama di versi ini: Menambahkan dukungan presensi, untuk ruangan Pesan Langsung (diingat bahwa presensi dinonaktifkan di matrix.org). Menambahkan lagi dukungan Android Auto.
+Perubahan utama di versi ini: Tambahkan dukungan presensi, untuk ruangan Pesan Langsung (diingat bahwa presensi dinonaktifkan di matrix.org). Tambahkan lagi dukungan Android Auto.
Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.4
diff --git a/fastlane/metadata/android/id/changelogs/40103050.txt b/fastlane/metadata/android/id/changelogs/40103050.txt
new file mode 100644
index 0000000000..ec7c9423bf
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Tambahkan dukungan presensi, untuk ruangan Pesan Langsung (diingat bahwa presensi dinonaktifkan di matrix.org). Tambahkan lagi dukungan Android Auto.
+Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/id/changelogs/40103060.txt b/fastlane/metadata/android/id/changelogs/40103060.txt
new file mode 100644
index 0000000000..4265699d2f
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Tambahkan dukungan presensi, untuk ruangan Pesan Langsung (catatan: presensi dinonaktifkan di matrix.org). Tambahkan lagi dukungan Android Auto.
+Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt
index dfa9c8c826..d28ae8b004 100644
--- a/fastlane/metadata/android/id/full_description.txt
+++ b/fastlane/metadata/android/id/full_description.txt
@@ -1,42 +1,42 @@
-Element adalah perpesanan yang aman dan aplikasi kolaborasi tim produktivitas yang ideal untuk obrolan grup saat bekerja jarak jauh. Aplikasi obrolan ini menggunakan enkripsi ujung-ke-ujung untuk memberikan konferensi video, berbagi file, dan panggilan suara.
+Element adalah perpesanan yang aman dan aplikasi kolaborasi tim produktivitas yang ideal untuk obrolan grup saat bekerja jarak jauh. Aplikasi perpesanan ini menggunakan enkripsi ujung-ke-ujung untuk memberikan konferensi video, pembagian file, dan panggilan suara yang aman.
-Fitur Element termasuk:
+Fitur Element termasuk
- Alat komunikasi online yang canggih
-- Pesan terenkripsi sepenuhnya untuk memungkinkan komunikasi perusahaan yang lebih aman, bahkan untuk pekerja jarak jauh
-- Obrolan terdesentralisasi berdasarkan framework sumber-terbuka Matrix
-- Berbagi file dengan aman dengan data terenkripsi saat mengelola proyek
-- Obrolan video dengan VoIP dan berbagi layar
+- Pesan-pesan yang dienkripsi sepenuhnya untuk memungkinkan komunikasi perusahaan yang lebih aman, bahkan untuk pekerja jarak jauh
+- Obrolan terdesentralisasi berdasarkan kerangka Matrix yang sumber terbuka
+- Pembagian file aman dengan data terenkripsi saat mengelola proyek
+- Obrolan video dengan VoIP dan pembagian layar
- Integrasi yang mudah dengan alat kolaborasi online favorit Anda, alat manajemen proyek, layanan VoIP dan aplikasi perpesanan tim lainnya
-Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Element beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Matrix memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka.
+Element benar-benar berbeda dari aplikasi perpesanan dan aplikasi kolaborasi lainnya. Element beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi.
-Pesan privasi dan terenkripsi
-Element melindungi Anda dari iklan yang tidak diinginkan, penambangan data dan taman berdinding. Element juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang ditandatangani secara silang.
+Perpesanan dengan privasi dan enkripsi
+Element melindungi Anda dari iklan yang tidak diinginkan, penambangan data dan taman berdinding. Element juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu dengan enkripsi ujung-ke-ujung dan verifikasi perangkat menggunakan penandatanganan silang.
-Element memberi Anda kendali atas privasi Anda sambil memungkinkan Anda untuk berkomunikasi dengan aman dengan siapa pun di jaringan Matrix, atau alat kolaborasi bisnis lainnya dengan mengintegrasikan dengan aplikasi seperti Slack.
+Element memberikan Anda kendali atas privasi Anda sambil memungkinkan Anda untuk berkomunikasi dengan siapa saja secara aman di jaringan Matrix, atau alat kolaborasi bisnis lainnya dengan mengintegrasikan aplikasi-aplikasi seperti Slack.
Element dapat dihost sendiri
-Untuk memungkinkan lebih banyak kendali atas data dan percakapan sensitif Anda, Element bisa dihost sendiri atau Anda dapat memilih host berbasis Matrix - standar untuk komunikasi terdesentralisasi sumber-terbuka. Element memberi Anda privasi, kepatuhan keamanan, dan fleksibilitas integrasi.
+Untuk memungkinkan lebih banyak kendali atas data dan pesan-pesan sensitif Anda, Element dapat dihost sendiri atau Anda dapat memilih host berbasis Matrix, standar untuk komunikasi terdesentralisasi sumber terbuka. Element memberi Anda privasi, kepatuhan keamanan, dan fleksibilitas integrasi.
Miliki data Anda
-Anda memutuskan di mana menyimpan data dan pesan Anda. Tanpa risiko penambangan data atau akses dari pihak ketiga.
+Anda memutuskan di mana untuk menyimpan data dan pesan-pesan Anda, tanpa risiko penambangan data atau akses dari pihak ketiga.
Element menempatkan Anda dalam kendali dengan cara yang berbeda:
1. Dapatkan akun gratis pada server publik matrix.org yang dihost oleh pengembang Matrix, atau memilih dari ribuan server publik yang dihost oleh sukarelawan
2. Host sendiri akun Anda dengan menjalankan server pada infrastruktur IT Anda sendiri
-3. Daftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services
+3. Daftar untuk akun di server khusus dengan berlangganan platform hosting Layanan Matrix Element
Pesan terbuka dan kolaborasi
-Anda dapat mengobrol dengan siapa saja di jaringan Matrix, apakah mereka menggunakan Element, aplikasi Matrix lain atau bahkan jika mereka menggunakan aplikasi perpesanan yang berbeda.
+Anda dapat mengobrol dengan siapa saja di jaringan Matrix, jika mereka menggunakan Element, aplikasi Matrix lain atau bahkan menggunakan aplikasi perpesanan yang berbeda.
Sangat aman
-Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat menggunakan penandatanganan-silang.
+Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam obrolan dapat mendekripsi pesan), dan verifikasi perangkat menggunakan penandatanganan silang.
Komunikasi dan integrasi lengkap
-Perpesanan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.
+Perpesanan, panggilan suara dan video, pembagian file, pembagian layar dan banyak integrasi bot dan widget. Buat ruangan dan komunitas, tetap terhubung dan selesaikan hal-hal penting.
Ambil di mana Anda tinggalkan
-Tetap terhubung di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io
+Tetap terhubung di mana Anda berada, dengan riwayat pesan yang disinkronkan di semua perangkat Anda dan web di https://app.element.io
-Open source
-Element Android adalah proyek sumber terbuka, di-host oleh GitHub. Silakan melaporkan bug dan/atau membuat kontribusi ke pengembangannya di https://github.com/vector-im/element-android
+Sumber terbuka
+Element Android adalah proyek sumber terbuka, dihost oleh GitHub. Silakan laporkan masalah yang Anda temukan, atau membuat kontribusi ke pengembangannya di https://github.com/vector-im/element-android
diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt
index e6c3a2f7a9..1cd770dd73 100644
--- a/fastlane/metadata/android/id/short_description.txt
+++ b/fastlane/metadata/android/id/short_description.txt
@@ -1 +1 @@
-Perpesanan grup - pesan terenkripsi, panggilan grup dan video
+Perpesanan grup - perpesanan, panggilan suara dan video grup terenkripsi
diff --git a/fastlane/metadata/android/it-IT/changelogs/40103050.txt b/fastlane/metadata/android/it-IT/changelogs/40103050.txt
new file mode 100644
index 0000000000..2762949682
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: aggiunto supporto alla presenza per messaggi diretti (nota: la presenza è disattivata su matrix.org). Aggiunto di nuovo il supporto ad Android Auto.
+Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/it-IT/changelogs/40103060.txt b/fastlane/metadata/android/it-IT/changelogs/40103060.txt
new file mode 100644
index 0000000000..f241fa9e57
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: aggiunto supporto alla presenza per i messaggi diretti (nota: la presenza è disattivata su matrix.org). Aggiunto di nuovo il supporto a Android Auto.
+Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103050.txt b/fastlane/metadata/android/pt-BR/changelogs/40103050.txt
new file mode 100644
index 0000000000..e565d269ed
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: Adicionar suporte a Presença, para sala de Mensagem Direta (nota: presença está desabilitada em matrix.org). Adicionar de novo suporte a Android Auto.
+Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103060.txt b/fastlane/metadata/android/pt-BR/changelogs/40103060.txt
new file mode 100644
index 0000000000..b246759d26
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: Adicionar suporte a Presença, para sala de Mensagem Direta (nota: presença está desabilitada em matrix.org). Adicionar de novo suporte a Android Auto.
+Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/sq/changelogs/40103040.txt b/fastlane/metadata/android/sq/changelogs/40103040.txt
index 7f37e82801..6ad044b6a4 100644
--- a/fastlane/metadata/android/sq/changelogs/40103040.txt
+++ b/fastlane/metadata/android/sq/changelogs/40103040.txt
@@ -1,2 +1,2 @@
-Ndryshimet kryesore në këtë version: Shtim mbulimi për Prani, për dhomë Mesazh i Drejtpërdrejtë (shënim: në matrix.org prania është e çaktivizuar. Shtim sërish i mbulimit për Android Auto.
+Ndryshimet kryesore në këtë version: Shtim mbulimi për Prani, për dhomë Mesazh i Drejtpërdrejtë (shënim: në matrix.org prania është e çaktivizuar). Shtim sërish i mbulimit për Android Auto.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.4
diff --git a/fastlane/metadata/android/sq/changelogs/40103050.txt b/fastlane/metadata/android/sq/changelogs/40103050.txt
new file mode 100644
index 0000000000..bb609da987
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Shtim mbulimi për Prani, për dhomën Mesazh i Drejtpërdrejtë (shënim: prania është e çaktivizuar në matrix.org). Shtim sërish i mbulimit për Android Auto.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/sq/changelogs/40103060.txt b/fastlane/metadata/android/sq/changelogs/40103060.txt
new file mode 100644
index 0000000000..96afd47a5d
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Shtim mbulimi për Prani, për dhomën Mesazh i Drejtpërdrejtë (shënim: prania është e çaktivizuar në matrix.org). Shtim sërish i mbulimit për Android Auto.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103050.txt b/fastlane/metadata/android/sv-SE/changelogs/40103050.txt
new file mode 100644
index 0000000000..57ee7189e3
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Lägg till närvarostöd för direktmeddelanden (obs: närvaro är inaktiverat på matrix.org). Lägg till stöd för Android Auto igen.
+Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103060.txt b/fastlane/metadata/android/sv-SE/changelogs/40103060.txt
new file mode 100644
index 0000000000..bac3775a2a
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Lägg till närvarostöd för direktmeddelanden (obs: närvaro är inaktiverat på matrix.org). Lägg till stöd för Android Auto igen.
+Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/uk/changelogs/40103040.txt b/fastlane/metadata/android/uk/changelogs/40103040.txt
index 3e65e0bc07..b6d237241b 100644
--- a/fastlane/metadata/android/uk/changelogs/40103040.txt
+++ b/fastlane/metadata/android/uk/changelogs/40103040.txt
@@ -1,2 +1,2 @@
-Основні зміни в цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнено на matrix.org. Знову додано підтримку Android Auto.
+Основні зміни в цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнено на matrix.org). Знову додано підтримку Android Auto.
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.4
diff --git a/fastlane/metadata/android/uk/changelogs/40103050.txt b/fastlane/metadata/android/uk/changelogs/40103050.txt
new file mode 100644
index 0000000000..846d1a2d84
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+Основні зміни у цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнена на matrix.org). Знову додано підтримку Android Auto.
+Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/uk/changelogs/40103060.txt b/fastlane/metadata/android/uk/changelogs/40103060.txt
new file mode 100644
index 0000000000..a1eec4d4de
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+Основні зміни у цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнена на matrix.org). Знову додано підтримку Android Auto.
+Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103040.txt b/fastlane/metadata/android/zh-CN/changelogs/40103040.txt
index 0c3d4d57c3..c879c3d036 100644
--- a/fastlane/metadata/android/zh-CN/changelogs/40103040.txt
+++ b/fastlane/metadata/android/zh-CN/changelogs/40103040.txt
@@ -1,2 +1,2 @@
-此版本主要变化:为 Direct Message 聊天室添加 Presence 支持 (注意:Presence 在matrix.org 上是禁用的。再次添加 Android Auto 支持。
+此版本主要变化:为 Direct Message 聊天室添加 Presence 支持 (注意:presence 在 matrix.org 上是禁用的)。再次添加 Android Auto 支持。
完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.4
diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103050.txt b/fastlane/metadata/android/zh-CN/changelogs/40103050.txt
new file mode 100644
index 0000000000..7343ae0b9f
--- /dev/null
+++ b/fastlane/metadata/android/zh-CN/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+此版本的主要变化:为私信聊天室添加 Presence 支持 (注意:在 matrix.org 上 Presence 是禁用的)。再次添加 Android Auto 支持。
+完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103060.txt b/fastlane/metadata/android/zh-CN/changelogs/40103060.txt
new file mode 100644
index 0000000000..8322539927
--- /dev/null
+++ b/fastlane/metadata/android/zh-CN/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+此版本的主要变化:为私信聊天室添加 Presence 支持(注意:在 matrix.org 上 Presence 是禁用的)。再次添加 Android Auto 支持。
+完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103050.txt b/fastlane/metadata/android/zh-TW/changelogs/40103050.txt
new file mode 100644
index 0000000000..659be479f5
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40103050.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.5
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103060.txt b/fastlane/metadata/android/zh-TW/changelogs/40103060.txt
new file mode 100644
index 0000000000..e1223a40e5
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40103060.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.6
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a37233c5e2..f5e3b23f79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
+distributionSha256Sum=00b273629df4ce46e68df232161d5a7c4e495b9a029ce6e0420f071e21316867
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml b/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml
new file mode 100644
index 0000000000..343c42ebf3
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/form_edit_text_hint_color_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml b/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml
new file mode 100644
index 0000000000..7079cc271b
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/form_edit_text_stroke_color_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index 03d1ff69db..9df2794a1a 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -129,9 +129,9 @@
@color/black_alpha@android:color/transparent
-
- @color/palette_black_900
- @color/palette_gray_400
+
+ @color/palette_black_900
+ @color/palette_gray_400
diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml
index ad90469a52..d09d0a399d 100644
--- a/library/ui-styles/src/main/res/values/styles_buttons.xml
+++ b/library/ui-styles/src/main/res/values/styles_buttons.xml
@@ -10,6 +10,11 @@
24sp
+
+
-
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_toast.xml b/library/ui-styles/src/main/res/values/styles_toast.xml
new file mode 100644
index 0000000000..22f4da7ac0
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/styles_toast.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml
index 59fea75074..2e87353303 100644
--- a/library/ui-styles/src/main/res/values/styles_voice_message.xml
+++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml
@@ -12,15 +12,4 @@
rightToLeft
-
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index d07e3c5297..4d525f7eee 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -140,8 +140,7 @@
@style/Widget.Vector.Keyword
-
- @color/vctr_voice_message_toast_background_dark
+ @color/vctr_toast_background_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 14ec372f28..790a0bfc7c 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -143,8 +143,7 @@
@style/Widget.Vector.Keyword
-
- @color/vctr_voice_message_toast_background_light
+ @color/vctr_toast_background_light
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
index 13fd097bcd..2a0abd3d24 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.pushers.Pusher
+import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
@@ -44,10 +45,10 @@ import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
class FlowSession(private val session: Session) {
- fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Flow> {
- return session.getRoomSummariesLive(queryParams).asFlow()
+ fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow> {
+ return session.getRoomSummariesLive(queryParams, sortOrder).asFlow()
.startWith(session.coroutineDispatchers.io) {
- session.getRoomSummaries(queryParams)
+ session.getRoomSummaries(queryParams, sortOrder)
}
}
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index deea87e8a3..94fc221cf1 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -9,7 +9,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath "io.realm:realm-gradle-plugin:10.8.0"
+ classpath "io.realm:realm-gradle-plugin:10.8.1"
}
}
@@ -31,7 +31,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- buildConfigField "String", "SDK_VERSION", "\"1.3.7\""
+ buildConfigField "String", "SDK_VERSION", "\"1.3.8\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
@@ -44,6 +44,7 @@ android {
}
testOptions {
+ // Comment to run on Android 12
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
@@ -106,8 +107,9 @@ dependencies {
implementation libs.androidx.appCompat
implementation libs.androidx.core
- implementation libs.androidx.lifecycleExtensions
- implementation libs.androidx.lifecycleJava8
+ // Lifecycle
+ implementation libs.androidx.lifecycleCommon
+ implementation libs.androidx.lifecycleProcess
// Network
implementation libs.squareup.retrofit
@@ -156,10 +158,10 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
testImplementation libs.tests.junit
- testImplementation 'org.robolectric:robolectric:4.6.1'
+ testImplementation 'org.robolectric:robolectric:4.7'
//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 libs.mockk.mockk
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
index dc4e0f152d..4a41eaec4a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
@@ -20,7 +20,18 @@ package org.matrix.android.sdk.api
* This class contains pattern to match Matrix Url, aka mxc urls
*/
object MatrixUrls {
+ /**
+ * "mxc" scheme, including "://". So "mxc://"
+ */
const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
+ /**
+ * Return true if the String starts with "mxc://"
+ */
fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME)
+
+ /**
+ * Remove the "mxc://" prefix. No op if the String is not a Mxc URL
+ */
+ fun String.removeMxcPrefix() = removePrefix(MATRIX_CONTENT_URI_SCHEME)
}
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
index b2035bb2eb..13a26c89c1 100644
--- 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
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
+import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection
@@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
error.code == MatrixError.M_INVALID_USERNAME ||
error.code == MatrixError.M_EXCLUSIVE)
}
+
+/**
+ * Try to convert to a ScanFailure. Return null in the cases it's not possible
+ */
+fun Throwable.toScanFailure(): ScanFailure? {
+ return if (this is Failure.OtherServerError) {
+ tryOrNull {
+ MoshiProvider.providesMoshi()
+ .adapter(ContentScannerError::class.java)
+ .fromJson(errorBody)
+ }
+ ?.let { ScanFailure(it, httpCode, this) }
+ } else {
+ null
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt
new file mode 100644
index 0000000000..466e345cad
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 org.matrix.android.sdk.api.session.events.model.Event
+
+data class PushEvents(
+ val matchedEvents: List>,
+ val roomsJoined: Collection,
+ val roomsLeft: Collection,
+ val redactedEventIds: List
+)
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
index 1d0acf38fa..88268f0f86 100644
--- 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
@@ -51,11 +51,7 @@ interface PushRuleService {
// fun fulfilledBingRule(event: Event, rules: List): PushRule?
interface PushRuleListener {
- fun onMatchRule(event: Event, actions: List)
- fun onRoomJoined(roomId: String)
- fun onRoomLeft(roomId: String)
- fun onEventRedacted(redactedEventId: String)
- fun batchFinish()
+ fun onEvents(pushEvents: PushEvents)
}
fun getKeywords(): LiveData>
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
index dfe43aed6f..3f817ec4d2 100644
--- 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
@@ -31,6 +31,7 @@ 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.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@@ -192,6 +193,11 @@ interface Session :
*/
fun cryptoService(): CryptoService
+ /**
+ * Returns the ContentScannerService associated with the session
+ */
+ fun contentScannerService(): ContentScannerService
+
/**
* Returns the identity service associated with the session
*/
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
index 36c471bb2b..3dd096e144 100644
--- 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
@@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.content
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+
/**
* This interface defines methods for accessing content from the current session.
*/
@@ -39,6 +41,15 @@ interface ContentUrlResolver {
*/
fun resolveFullSize(contentUrl: String?): String?
+ /**
+ * Get the ResolvedMethod to download a URL
+ *
+ * @param contentUrl the Matrix media content URI (in the form of "mxc://...").
+ * @param elementToDecrypt Encryption data may be required if you use a content scanner
+ * @return the Method to access resource, or null if invalid
+ */
+ fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt? = null): ResolvedMethod?
+
/**
* Get the actual URL for accessing the thumbnail image of a given Matrix media content URI.
*
@@ -49,4 +60,9 @@ interface ContentUrlResolver {
* @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?
+
+ sealed class ResolvedMethod {
+ data class GET(val url: String) : ResolvedMethod()
+ data class POST(val url: String, val jsonBody: String) : ResolvedMethod()
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt
new file mode 100644
index 0000000000..cef5d41f2c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.contentscanner
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ContentScannerError(
+ @Json(name = "info") val info: String? = null,
+ @Json(name = "reason") val reason: String? = null
+) {
+ companion object {
+ // 502 The server failed to request media from the media repo.
+ const val REASON_MCS_MEDIA_REQUEST_FAILED = "MCS_MEDIA_REQUEST_FAILED"
+
+ /* 400 The server failed to decrypt the encrypted media downloaded from the media repo.*/
+ const val REASON_MCS_MEDIA_FAILED_TO_DECRYPT = "MCS_MEDIA_FAILED_TO_DECRYPT"
+
+ /* 403 The server scanned the downloaded media but the antivirus script returned a non-zero exit code.*/
+ const val REASON_MCS_MEDIA_NOT_CLEAN = "MCS_MEDIA_NOT_CLEAN"
+
+ /* 403 The provided encrypted_body could not be decrypted. The client should request the public key of the server and then retry (once).*/
+ const val REASON_MCS_BAD_DECRYPTION = "MCS_BAD_DECRYPTION"
+
+ /* 400 The request body contains malformed JSON.*/
+ const val REASON_MCS_MALFORMED_JSON = "MCS_MALFORMED_JSON"
+ }
+}
+
+class ScanFailure(val error: ContentScannerError, val httpCode: Int, cause: Throwable? = null) : Throwable(cause = cause)
+
+// For Glide, which deals with Exception and not with Throwable
+fun ScanFailure.toException() = Exception(this)
+fun Throwable.toScanFailure() = this.cause as? ScanFailure
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
new file mode 100644
index 0000000000..1dd7bab01c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.contentscanner
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+
+interface ContentScannerService {
+
+ val serverPublicKey: String?
+
+ fun getContentScannerServer(): String?
+ fun setScannerUrl(url: String?)
+ fun enableScanner(enabled: Boolean)
+ fun isScannerEnabled(): Boolean
+ fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean = true, fileInfo: ElementToDecrypt? = null): LiveData>
+ fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo?
+
+ /**
+ * Get the current public curve25519 key that the AV server is advertising.
+ * @param callback on success callback containing the server public key
+ */
+ suspend fun getServerPublicKey(forceDownload: Boolean = false): String?
+ suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo
+}
diff --git a/vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt
similarity index 55%
rename from vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt
index a82e5a4e03..da209080ac 100644
--- a/vector/src/main/java/im/vector/app/core/utils/DefaultSubscriber.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 New Vector Ltd
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package im.vector.app.core.utils
+package org.matrix.android.sdk.api.session.contentscanner
-import io.reactivex.Completable
-import io.reactivex.Single
-import io.reactivex.disposables.Disposable
-import io.reactivex.internal.functions.Functions
-import timber.log.Timber
-
-fun Single.subscribeLogError(): Disposable {
- return subscribe(Functions.emptyConsumer(), { Timber.e(it) })
+enum class ScanState {
+ TRUSTED,
+ INFECTED,
+ UNKNOWN,
+ IN_PROGRESS
}
-fun Completable.subscribeLogError(): Disposable {
- return subscribe({}, { Timber.e(it) })
-}
+data class ScanStatusInfo(
+ val state: ScanState,
+ val scanDateTimestamp: Long?,
+ val humanReadableMessage: String?
+)
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
index d0ce55070e..a39ca5b4f4 100644
--- 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
@@ -102,6 +102,9 @@ object EventType {
// Relation Events
const val REACTION = "m.reaction"
+ // Poll
+ const val POLL_START = "org.matrix.msc3381.poll.start"
+
// Unwedging
internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
new file mode 100644
index 0000000000..ef2fd1867a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 MessagePollContent(
+ @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
new file mode 100644
index 0000000000..8f5ff53c85
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 PollAnswer(
+ @Json(name = "id") val id: String? = null,
+ @Json(name = "org.matrix.msc1767.text") val answer: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
new file mode 100644
index 0000000000..e652514b92
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 PollCreationInfo(
+ @Json(name = "question") val question: PollQuestion? = null,
+ @Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed",
+ @Json(name = "max_selections") val maxSelections: Int = 1,
+ @Json(name = "answers") val answers: List? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
new file mode 100644
index 0000000000..76025f745e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 PollQuestion(
+ @Json(name = "org.matrix.msc1767.text") val question: String? = null
+)
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
index 6ae42de90c..a2b38b6606 100644
--- 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
@@ -20,7 +20,6 @@ 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
@@ -84,10 +83,10 @@ interface SendService {
/**
* Send a poll to the room.
* @param question the question
- * @param options list of (label, value)
+ * @param options list of options
* @return a [Cancelable]
*/
- fun sendPoll(question: String, options: List): Cancelable
+ fun sendPoll(question: String, options: List): Cancelable
/**
* Method to send a poll response.
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
index 4a6462477d..86cb10bfe8 100644
--- 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
@@ -28,8 +28,10 @@ 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.message.MessageTextContent
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
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
/**
@@ -131,20 +133,6 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
}
}
-/**
- * Get last Message body, after a possible edition
- */
-fun TimelineEvent.getLastMessageBody(): String? {
- val lastMessageContent = getLastMessageContent()
-
- if (lastMessageContent != null) {
- return lastMessageContent.newContent?.toModel()?.body
- ?: lastMessageContent.body
- }
-
- return null
-}
-
/**
* Returns true if it's a reply
*/
@@ -156,11 +144,25 @@ fun TimelineEvent.isEdition(): Boolean {
return root.isEdition()
}
-fun TimelineEvent.getTextEditableContent(): String? {
- val lastContent = getLastMessageContent()
+/**
+ * Get the latest message body, after a possible edition, stripping the reply prefix if necessary
+ */
+fun TimelineEvent.getTextEditableContent(): String {
+ val lastContentBody = getLastMessageContent()?.body ?: return ""
return if (isReply()) {
- return extractUsefulTextFromReply(lastContent?.body ?: "")
+ extractUsefulTextFromReply(lastContentBody)
} else {
- lastContent?.body ?: ""
+ lastContentBody
}
}
+
+/**
+ * Get the latest displayable content.
+ * Will take care to hide spoiler text
+ */
+fun MessageContent.getTextDisplayableContent(): String {
+ return newContent?.toModel()?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
+ ?: newContent?.toModel()?.body
+ ?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
+ ?: body
+}
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
index 1a00b85ff4..e453cb2df5 100644
--- 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
@@ -15,6 +15,8 @@
*/
package org.matrix.android.sdk.api.util
+import org.matrix.android.sdk.internal.util.unescapeHtml
+
object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
@@ -44,4 +46,15 @@ object ContentUtils {
}
return repliedBody
}
+
+ @Suppress("RegExpRedundantEscape")
+ fun formatSpoilerTextFromHtml(formattedBody: String): String {
+ // var reason = "",
+ // can capture the spoiler reason for better formatting? ex. { reason = it.value; ">"}
+ return formattedBody.replace("(?<=".toRegex(), ">")
+ .replace("(?<=).+?(?=)".toRegex()) { SPOILER_CHAR.repeat(it.value.length) }
+ .unescapeHtml()
+ }
+
+ private const val SPOILER_CHAR = "█"
}
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
index c746ad863a..934d61de45 100644
--- 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
@@ -51,6 +51,11 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
}
}
+ fun stopSession(sessionId: String) {
+ val sessionComponent = sessionComponents[sessionId] ?: throw RuntimeException("You don't have a session for id $sessionId")
+ sessionComponent.session().stopSync()
+ }
+
fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
DaggerSessionComponent
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
index b72cff3cf1..0583951138 100644
--- 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
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistration
import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
+import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
@@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI,
- DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
+ DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
)
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
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
index f71c5079b3..9f425eee7f 100755
--- 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
@@ -38,14 +38,22 @@ data class MXKey(
/**
* signature user Id to [deviceid][signature]
*/
- private val signatures: Map>
+ private val signatures: Map>,
+
+ /**
+ * We have to store the original json because it can contain other fields
+ * that we don't support yet but they would be needed to check signatures
+ */
+ private val rawMap: JsonDict
) {
/**
* @return the signed data map
*/
fun signalableJSONDictionary(): Map {
- return mapOf("key" to value)
+ return rawMap.filter {
+ it.key != "signatures" && it.key != "unsigned"
+ }
}
/**
@@ -82,6 +90,7 @@ data class MXKey(
*
* "signed_curve25519:AAAAFw": {
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
+ * "fallback" : true|false
* "signatures": {
* "@userId:matrix.org": {
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
@@ -107,7 +116,8 @@ data class MXKey(
type = components[0],
keyId = components[1],
value = params["key"] as String,
- signatures = params["signatures"] as Map>
+ signatures = params["signatures"] as Map>,
+ rawMap = params
)
}
}
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
index 49e155add8..8b78cd0d42 100644
--- 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
@@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class IdentityDatabase
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class ContentScannerDatabase
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
index 361a306d4f..1ab1042129 100644
--- 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
@@ -38,6 +38,9 @@ internal object NetworkConstants {
// Integration
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
+ // Content scanner
+ const val URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/"
+
// Federation
const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
}
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
index 46c5967876..14dfc097cf 100644
--- 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
@@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.completeWith
import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@@ -118,12 +120,24 @@ internal class DefaultFileService @Inject constructor(
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
if (!cachedFiles.file.exists()) {
- val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
+ val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")
- val request = Request.Builder()
- .url(resolvedUrl)
- .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
- .build()
+ val request = when (resolvedUrl) {
+ is ContentUrlResolver.ResolvedMethod.GET -> {
+ Request.Builder()
+ .url(resolvedUrl.url)
+ .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
+ .build()
+ }
+
+ is ContentUrlResolver.ResolvedMethod.POST -> {
+ Request.Builder()
+ .url(resolvedUrl.url)
+ .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
+ .post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType()))
+ .build()
+ }
+ }
val response = try {
okHttpClient.newCall(request).execute()
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
index c52462612a..c07ff48cf4 100644
--- 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
@@ -35,6 +35,7 @@ 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.contentscanner.ContentScannerService
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy,
private val accountService: Lazy,
private val eventService: Lazy,
+ private val contentScannerService: Lazy,
private val identityService: IdentityService,
private val integrationManagerService: IntegrationManagerService,
private val thirdPartyService: Lazy,
@@ -174,8 +176,8 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach {
it.onSessionStarted(this)
}
- sessionListeners.dispatch { _, listener ->
- listener.onSessionStarted(this)
+ dispatchTo(sessionListeners) { session, listener ->
+ listener.onSessionStarted(session)
}
}
}
@@ -217,8 +219,8 @@ internal class DefaultSession @Inject constructor(
// timelineEventDecryptor.destroy()
uiHandler.post {
lifecycleObservers.forEach { it.onSessionStopped(this) }
- sessionListeners.dispatch { _, listener ->
- listener.onSessionStopped(this)
+ dispatchTo(sessionListeners) { session, listener ->
+ listener.onSessionStopped(session)
}
}
cryptoService.get().close()
@@ -249,8 +251,8 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach {
it.onClearCache(this)
}
- sessionListeners.dispatch { _, listener ->
- listener.onClearCache(this)
+ dispatchTo(sessionListeners) { session, listener ->
+ listener.onClearCache(session)
}
}
withContext(NonCancellable) {
@@ -260,8 +262,8 @@ internal class DefaultSession @Inject constructor(
}
override fun onGlobalError(globalError: GlobalError) {
- sessionListeners.dispatch { _, listener ->
- listener.onGlobalError(this, globalError)
+ dispatchTo(sessionListeners) { session, listener ->
+ listener.onGlobalError(session, globalError)
}
}
@@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
override fun cryptoService(): CryptoService = cryptoService.get()
+ override fun contentScannerService(): ContentScannerService = contentScannerService.get()
+
override fun identityService() = identityService
override fun fileService(): FileService = defaultFileService.get()
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
index bc8a707530..76e5d84e56 100644
--- 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
@@ -36,6 +36,7 @@ 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.contentscanner.ContentScannerModule
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
@@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
AccountModule::class,
FederationModule::class,
CallModule::class,
+ ContentScannerModule::class,
SearchModule::class,
ThirdPartyModule::class,
SpaceModule::class,
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
index d5c661b1e4..756b9cef83 100644
--- 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
@@ -18,15 +18,11 @@ package org.matrix.android.sdk.internal.session
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.di.SessionId
import timber.log.Timber
import javax.inject.Inject
@SessionScope
-internal class SessionListeners @Inject constructor(
- @SessionId private val sessionId: String,
- private val sessionManager: SessionManager) {
+internal class SessionListeners @Inject constructor() {
private val listeners = mutableSetOf()
@@ -42,18 +38,19 @@ internal class SessionListeners @Inject constructor(
}
}
- fun dispatch(block: (Session, Session.Listener) -> Unit) {
+ fun dispatch(session: Session, block: (Session, Session.Listener) -> Unit) {
synchronized(listeners) {
- val session = getSession() ?: return Unit.also {
- Timber.w("You don't have any attached session")
- }
listeners.forEach {
tryOrNull { block(session, it) }
}
}
}
-
- private fun getSession(): Session? {
- return sessionManager.getSessionComponent(sessionId)?.session()
- }
+}
+
+internal fun Session?.dispatchTo(sessionListeners: SessionListeners, block: (Session, Session.Listener) -> Unit) {
+ if (this == null) {
+ Timber.w("You don't have any attached session")
+ return
+ }
+ sessionListeners.dispatch(this, block)
}
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
index 1a8e80ab68..752856b931 100644
--- 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
@@ -44,7 +44,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
-
+ cleanupSession.stopActiveTasks()
val canCleanup = try {
executeRequest(globalErrorReceiver) {
accountAPI.deactivate(deactivateAccountParams)
@@ -71,7 +71,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
- cleanupSession.handle()
+ cleanupSession.cleanup()
}
}
}
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
index e8d3eb1a78..c42141a0aa 100644
--- 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
@@ -50,20 +50,26 @@ internal class CleanupSession @Inject constructor(
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String
) {
- suspend fun handle() {
+
+ fun stopActiveTasks() {
+ Timber.d("Cleanup: cancel pending works...")
+ workManagerProvider.cancelAllWorks()
+
+ Timber.d("Cleanup: stop session...")
+ sessionManager.stopSession(sessionId)
+ }
+
+ suspend fun cleanup() {
val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)")
- Timber.d("Cleanup: delete session params...")
- sessionParamsStore.delete(sessionId)
-
- Timber.d("Cleanup: cancel pending works...")
- workManagerProvider.cancelAllWorks()
-
Timber.d("Cleanup: release session...")
sessionManager.releaseSession(sessionId)
+ Timber.d("Cleanup: delete session params...")
+ sessionParamsStore.delete(sessionId)
+
Timber.d("Cleanup: clear session data...")
clearSessionDataTask.execute(Unit)
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
index 5c8cf99dc6..660ab8726f 100644
--- 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
@@ -16,20 +16,45 @@
package org.matrix.android.sdk.internal.session.content
-import org.matrix.android.sdk.api.MatrixUrls
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
+import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.network.NetworkConstants
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import javax.inject.Inject
-internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
+internal class DefaultContentUrlResolver @Inject constructor(
+ homeServerConnectionConfig: HomeServerConnectionConfig,
+ private val scannerService: ContentScannerService
+) : ContentUrlResolver {
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
+ override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
+ return if (scannerService.isScannerEnabled() && elementToDecrypt != null) {
+ val baseUrl = scannerService.getContentScannerServer()
+ val sep = if (baseUrl?.endsWith("/") == true) "" else "/"
+
+ val url = baseUrl + sep + NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted"
+
+ ContentUrlResolver.ResolvedMethod.POST(
+ url = url,
+ jsonBody = ScanEncryptorUtils
+ .getDownloadBodyAndEncryptIfNeeded(scannerService.serverPublicKey, contentUrl ?: "", elementToDecrypt)
+ .toJson()
+ )
+ } else {
+ resolveFullSize(contentUrl)?.let { ContentUrlResolver.ResolvedMethod.GET(it) }
+ }
+ }
+
override fun resolveFullSize(contentUrl: String?): String? {
return contentUrl
// do not allow non-mxc content URLs
@@ -37,7 +62,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let {
resolve(
contentUrl = it,
- prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
+ toThumbnail = false
)
}
}
@@ -49,16 +74,27 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let {
resolve(
contentUrl = it,
- prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
+ toThumbnail = true,
params = "?width=$width&height=$height&method=${method.value}"
)
}
}
private fun resolve(contentUrl: String,
- prefix: String,
- params: String = ""): String? {
- var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME)
+ toThumbnail: Boolean,
+ params: String = ""): String {
+ var serverAndMediaId = contentUrl.removeMxcPrefix()
+
+ val apiPath = if (scannerService.isScannerEnabled()) {
+ NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
+ } else {
+ NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
+ }
+ val prefix = if (toThumbnail) {
+ apiPath + "thumbnail/"
+ } else {
+ apiPath + "download/"
+ }
val fragmentOffset = serverAndMediaId.indexOf("#")
var fragment = ""
if (fragmentOffset >= 0) {
@@ -66,6 +102,11 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
}
- return baseUrl + prefix + serverAndMediaId + params + fragment
+ val resolvedUrl = if (scannerService.isScannerEnabled()) {
+ scannerService.getContentScannerServer()!!.ensureTrailingSlash()
+ } else {
+ baseUrl
+ }
+ return resolvedUrl + prefix + serverAndMediaId + params + fragment
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt
new file mode 100644
index 0000000000..46f1705806
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import okhttp3.ResponseBody
+import org.matrix.android.sdk.internal.network.NetworkConstants
+import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+/**
+ * https://github.com/matrix-org/matrix-content-scanner
+ */
+internal interface ContentScannerApi {
+
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted")
+ suspend fun downloadEncrypted(@Body info: DownloadBody): ResponseBody
+
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan_encrypted")
+ suspend fun scanFile(@Body info: DownloadBody): ScanResponse
+
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "public_key")
+ suspend fun getServerPublicKey(): ServerPublicKeyResponse
+
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan/{domain}/{mediaId}")
+ suspend fun scanMedia(@Path(value = "domain") domain: String, @Path(value = "mediaId") mediaId: String): ScanResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt
new file mode 100644
index 0000000000..d8548bb238
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import org.matrix.android.sdk.internal.session.SessionScope
+import javax.inject.Inject
+
+@SessionScope
+internal class ContentScannerApiProvider @Inject constructor() {
+ var contentScannerApi: ContentScannerApi? = null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt
new file mode 100644
index 0000000000..7ea74225cd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.internal.database.RealmKeysUtils
+import org.matrix.android.sdk.internal.di.ContentScannerDatabase
+import org.matrix.android.sdk.internal.di.SessionFilesDirectory
+import org.matrix.android.sdk.internal.di.UserMd5
+import org.matrix.android.sdk.internal.session.SessionModule
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.db.ContentScannerRealmModule
+import org.matrix.android.sdk.internal.session.contentscanner.db.RealmContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultDownloadEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultGetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanMediaTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DownloadEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
+import java.io.File
+
+@Module
+internal abstract class ContentScannerModule {
+ @Module
+ companion object {
+
+ @JvmStatic
+ @Provides
+ @ContentScannerDatabase
+ @SessionScope
+ fun providesContentScannerRealmConfiguration(realmKeysUtils: RealmKeysUtils,
+ @SessionFilesDirectory directory: File,
+ @UserMd5 userMd5: String): RealmConfiguration {
+ return RealmConfiguration.Builder()
+ .directory(directory)
+ .name("matrix-sdk-content-scanning.realm")
+ .apply {
+ realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
+ }
+ .allowWritesOnUiThread(true)
+ .modules(ContentScannerRealmModule())
+ .build()
+ }
+ }
+
+ @Binds
+ abstract fun bindContentScannerService(service: DisabledContentScannerService): ContentScannerService
+
+ @Binds
+ abstract fun bindContentScannerStore(store: RealmContentScannerStore): ContentScannerStore
+
+ @Binds
+ abstract fun bindDownloadEncryptedTask(task: DefaultDownloadEncryptedTask): DownloadEncryptedTask
+
+ @Binds
+ abstract fun bindGetServerPublicKeyTask(task: DefaultGetServerPublicKeyTask): GetServerPublicKeyTask
+
+ @Binds
+ abstract fun bindScanMediaTask(task: DefaultScanMediaTask): ScanMediaTask
+
+ @Binds
+ abstract fun bindScanEncryptedTask(task: DefaultScanEncryptedTask): ScanEncryptedTask
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
new file mode 100644
index 0000000000..4ecb337603
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import androidx.lifecycle.LiveData
+import dagger.Lazy
+import kotlinx.coroutines.launch
+import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+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 org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
+import org.matrix.android.sdk.internal.task.TaskExecutor
+import timber.log.Timber
+import javax.inject.Inject
+
+@SessionScope
+internal class DefaultContentScannerService @Inject constructor(
+ private val retrofitFactory: RetrofitFactory,
+ @Unauthenticated
+ private val okHttpClient: Lazy,
+ private val contentScannerApiProvider: ContentScannerApiProvider,
+ private val contentScannerStore: ContentScannerStore,
+ private val getServerPublicKeyTask: GetServerPublicKeyTask,
+ private val scanEncryptedTask: ScanEncryptedTask,
+ private val scanMediaTask: ScanMediaTask,
+ private val taskExecutor: TaskExecutor
+) : ContentScannerService {
+
+ // Cache public key in memory
+ override var serverPublicKey: String? = null
+ private set
+
+ override fun getContentScannerServer(): String? {
+ return contentScannerStore.getScannerUrl()
+ }
+
+ override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
+ val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException("No content scanner define")
+
+ if (!forceDownload && serverPublicKey != null) {
+ return serverPublicKey
+ }
+
+ return getServerPublicKeyTask.execute(GetServerPublicKeyTask.Params(api)).also {
+ serverPublicKey = it
+ }
+ }
+
+ override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
+ val result = if (fileInfo != null) {
+ scanEncryptedTask.execute(ScanEncryptedTask.Params(
+ mxcUrl = mxcUrl,
+ publicServerKey = getServerPublicKey(false),
+ encryptedInfo = fileInfo
+ ))
+ } else {
+ scanMediaTask.execute(ScanMediaTask.Params(mxcUrl))
+ }
+
+ return ScanStatusInfo(
+ state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
+ humanReadableMessage = result.info,
+ scanDateTimestamp = System.currentTimeMillis()
+ )
+ }
+
+ override fun setScannerUrl(url: String?) = contentScannerStore.setScannerUrl(url).also {
+ if (url == null) {
+ contentScannerApiProvider.contentScannerApi = null
+ serverPublicKey = null
+ } else {
+ val api = retrofitFactory
+ .create(okHttpClient, url)
+ .create(ContentScannerApi::class.java)
+ contentScannerApiProvider.contentScannerApi = api
+
+ taskExecutor.executorScope.launch {
+ try {
+ getServerPublicKey(true)
+ } catch (failure: Throwable) {
+ Timber.e("Failed to get public server api")
+ }
+ }
+ }
+ }
+
+ override fun enableScanner(enabled: Boolean) = contentScannerStore.enableScanner(enabled)
+
+ override fun isScannerEnabled(): Boolean = contentScannerStore.isScanEnabled()
+
+ override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
+ return contentScannerStore.getScanResult(mxcUrl)
+ }
+
+ override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData> {
+ val data = contentScannerStore.getLiveScanResult(mxcUrl)
+ if (fetchIfNeeded && !contentScannerStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
+ taskExecutor.executorScope.launch {
+ try {
+ getScanResultForAttachment(mxcUrl, fileInfo)
+ } catch (failure: Throwable) {
+ Timber.e("Failed to get file status : ${failure.localizedMessage}")
+ }
+ }
+ }
+ return data
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
new file mode 100644
index 0000000000..9087c71566
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.session.SessionScope
+import javax.inject.Inject
+
+/**
+ * Created to by-pass ProfileTask execution in LoginWizard.
+ */
+@SessionScope
+internal class DisabledContentScannerService @Inject constructor() : ContentScannerService {
+
+ override val serverPublicKey: String?
+ get() = null
+
+ override fun getContentScannerServer(): String? {
+ return null
+ }
+
+ override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
+ return null
+ }
+
+ override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
+ TODO("Not yet implemented")
+ }
+
+ override fun setScannerUrl(url: String?) {
+ }
+
+ override fun enableScanner(enabled: Boolean) {
+ }
+
+ override fun isScannerEnabled(): Boolean {
+ return false
+ }
+
+ override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData> {
+ return MutableLiveData()
+ }
+
+ override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
+ return null
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
new file mode 100644
index 0000000000..8fc84a487e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
+import org.matrix.android.sdk.internal.crypto.tools.withOlmEncryption
+import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.EncryptedBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.toCanonicalJson
+
+internal object ScanEncryptorUtils {
+
+ fun getDownloadBodyAndEncryptIfNeeded(publicServerKey: String?, mxcUrl: String, elementToDecrypt: ElementToDecrypt): DownloadBody {
+ // TODO, upstream refactoring changed the object model here...
+ // it's bad we have to recreate and use hardcoded values
+ val encryptedInfo = EncryptedFileInfo(
+ url = mxcUrl,
+ iv = elementToDecrypt.iv,
+ hashes = mapOf("sha256" to elementToDecrypt.sha256),
+ key = EncryptedFileKey(
+ k = elementToDecrypt.k,
+ alg = "A256CTR",
+ keyOps = listOf("encrypt", "decrypt"),
+ kty = "oct",
+ ext = true
+ ),
+ v = "v2"
+ )
+ return if (publicServerKey != null) {
+ // We should encrypt
+ withOlmEncryption { olm ->
+ olm.setRecipientKey(publicServerKey)
+
+ val olmResult = olm.encrypt(DownloadBody(encryptedInfo).toCanonicalJson())
+ DownloadBody(
+ encryptedBody = EncryptedBody(
+ cipherText = olmResult.mCipherText,
+ ephemeral = olmResult.mEphemeralKey,
+ mac = olmResult.mMac
+ )
+ )
+ }
+ } else {
+ DownloadBody(encryptedInfo)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt
new file mode 100644
index 0000000000..5cfe851a5c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.data
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+
+internal interface ContentScannerStore {
+
+ fun getScannerUrl(): String?
+
+ fun setScannerUrl(url: String?)
+
+ fun enableScanner(enabled: Boolean)
+
+ fun isScanEnabled(): Boolean
+
+ fun getScanResult(mxcUrl: String): ScanStatusInfo?
+ fun getLiveScanResult(mxcUrl: String): LiveData>
+ fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean
+
+ fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?)
+ fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt
new file mode 100644
index 0000000000..0ffff441f8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.RealmObject
+import io.realm.annotations.Index
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+
+internal open class ContentScanResultEntity(
+ @Index
+ var mediaUrl: String? = null,
+ var scanStatusString: String? = null,
+ var humanReadableMessage: String? = null,
+ var scanDateTimestamp: Long? = null,
+ var scannerUrl: String? = null
+) : RealmObject() {
+
+ var scanResult: ScanState
+ get() {
+ return scanStatusString
+ ?.let {
+ tryOrNull { ScanState.valueOf(it) }
+ }
+ ?: ScanState.UNKNOWN
+ }
+ set(result) {
+ scanStatusString = result.name
+ }
+
+ fun toModel(): ScanStatusInfo {
+ return ScanStatusInfo(
+ state = this.scanResult,
+ humanReadableMessage = humanReadableMessage,
+ scanDateTimestamp = scanDateTimestamp
+ )
+ }
+
+ companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt
new file mode 100644
index 0000000000..b47be235c6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+
+internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity? {
+ return realm.where()
+ .equalTo(ContentScanResultEntityFields.MEDIA_URL, attachmentUrl)
+ .apply {
+ contentScannerUrl?.let {
+ equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
+ }
+ }
+ .findFirst()
+}
+
+internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity {
+ return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl)
+ ?: realm.createObject().also {
+ it.mediaUrl = attachmentUrl
+ it.scanDateTimestamp = System.currentTimeMillis()
+ it.scannerUrl = contentScannerUrl
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt
new file mode 100644
index 0000000000..d1910de36a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.RealmObject
+
+internal open class ContentScannerInfoEntity(
+ var serverUrl: String? = null,
+ var enabled: Boolean? = null
+) : RealmObject() {
+
+ companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt
new file mode 100644
index 0000000000..bb53140ad9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.annotations.RealmModule
+
+/**
+ * Realm module for content scanner classes
+ */
+@RealmModule(library = true,
+ classes = [
+ ContentScannerInfoEntity::class,
+ ContentScanResultEntity::class
+ ])
+internal class ContentScannerRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt
new file mode 100644
index 0000000000..947a66c8b9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.di.ContentScannerDatabase
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.util.isValidUrl
+import javax.inject.Inject
+
+@SessionScope
+internal class RealmContentScannerStore @Inject constructor(
+ @ContentScannerDatabase
+ private val realmConfiguration: RealmConfiguration
+) : ContentScannerStore {
+
+ private val monarchy = Monarchy.Builder()
+ .setRealmConfiguration(realmConfiguration)
+ .build()
+
+ override fun getScannerUrl(): String? {
+ return monarchy.fetchAllMappedSync(
+ { realm ->
+ realm.where()
+ }, {
+ it.serverUrl
+ }
+ ).firstOrNull()
+ }
+
+ override fun setScannerUrl(url: String?) {
+ monarchy.runTransactionSync { realm ->
+ val info = realm.where().findFirst()
+ ?: realm.createObject()
+ info.serverUrl = url
+ }
+ }
+
+ override fun enableScanner(enabled: Boolean) {
+ monarchy.runTransactionSync { realm ->
+ val info = realm.where().findFirst()
+ ?: realm.createObject()
+ info.enabled = enabled
+ }
+ }
+
+ override fun isScanEnabled(): Boolean {
+ return monarchy.fetchAllMappedSync(
+ { realm ->
+ realm.where()
+ }, {
+ it.enabled.orFalse() && it.serverUrl?.isValidUrl().orFalse()
+ }
+ ).firstOrNull().orFalse()
+ }
+
+ override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) {
+ monarchy.runTransactionSync {
+ ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state
+ }
+ }
+
+ override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) {
+ monarchy.runTransactionSync {
+ ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply {
+ scanResult = state
+ scanDateTimestamp = System.currentTimeMillis()
+ humanReadableMessage = humanReadable
+ }
+ }
+ }
+
+ override fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean {
+ var isKnown = false
+ monarchy.runTransactionSync {
+ val info = ContentScanResultEntity.get(it, mxcUrl, scannerUrl)?.scanResult
+ isKnown = when (info) {
+ ScanState.IN_PROGRESS,
+ ScanState.TRUSTED,
+ ScanState.INFECTED -> true
+ else -> false
+ }
+ }
+ return isKnown
+ }
+
+ override fun getScanResult(mxcUrl: String): ScanStatusInfo? {
+ return monarchy.fetchAllMappedSync({ realm ->
+ realm.where()
+ .equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
+ .apply {
+ getScannerUrl()?.let {
+ equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
+ }
+ }
+ }, {
+ it.toModel()
+ })
+ .firstOrNull()
+ }
+
+ override fun getLiveScanResult(mxcUrl: String): LiveData> {
+ val liveData = monarchy.findAllMappedWithChanges(
+ { realm: Realm ->
+ realm.where()
+ .equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
+ .equalTo(ContentScanResultEntityFields.SCANNER_URL, getScannerUrl())
+ },
+ { entity ->
+ entity.toModel()
+ }
+ )
+ return Transformations.map(liveData) {
+ it.firstOrNull().toOptional()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
new file mode 100644
index 0000000000..5bac96a0c0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.JsonCanonicalizer
+
+@JsonClass(generateAdapter = true)
+internal data class DownloadBody(
+ @Json(name = "file") val file: EncryptedFileInfo? = null,
+ @Json(name = "encrypted_body") val encryptedBody: EncryptedBody? = null
+)
+
+@JsonClass(generateAdapter = true)
+internal data class EncryptedBody(
+ @Json(name = "ciphertext") val cipherText: String,
+ @Json(name = "mac") val mac: String,
+ @Json(name = "ephemeral") val ephemeral: String
+)
+
+internal fun DownloadBody.toJson(): String = MoshiProvider.providesMoshi().adapter(DownloadBody::class.java).toJson(this)
+
+internal fun DownloadBody.toCanonicalJson() = JsonCanonicalizer.getCanonicalJson(DownloadBody::class.java, this)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt
new file mode 100644
index 0000000000..f783fe0a6c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * {
+ * "clean": true,
+ * "info": "File clean at 6/7/2018, 6:02:40 PM"
+ * }
+ */
+@JsonClass(generateAdapter = true)
+internal data class ScanResponse(
+ @Json(name = "clean") val clean: Boolean,
+ /** Human-readable information about the result. */
+ @Json(name = "info") val info: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt
new file mode 100644
index 0000000000..2e97a85bca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ServerPublicKeyResponse(
+ @Json(name = "public_key")
+ val publicKey: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
new file mode 100644
index 0000000000..f92c869cb8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import okhttp3.ResponseBody
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface DownloadEncryptedTask : Task {
+ data class Params(
+ val publicServerKey: String?,
+ val encryptedInfo: ElementToDecrypt,
+ val mxcUrl: String
+ )
+}
+
+internal class DefaultDownloadEncryptedTask @Inject constructor(
+ private val contentScannerApiProvider: ContentScannerApiProvider
+) : DownloadEncryptedTask {
+
+ override suspend fun execute(params: DownloadEncryptedTask.Params): ResponseBody {
+ val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(
+ params.publicServerKey,
+ params.mxcUrl,
+ params.encryptedInfo
+ )
+
+ val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+ return executeRequest(null) {
+ api.downloadEncrypted(dlBody)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt
new file mode 100644
index 0000000000..41c2ec9c38
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApi
+import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetServerPublicKeyTask : Task {
+ data class Params(
+ val contentScannerApi: ContentScannerApi
+ )
+}
+
+internal class DefaultGetServerPublicKeyTask @Inject constructor() : GetServerPublicKeyTask {
+
+ override suspend fun execute(params: GetServerPublicKeyTask.Params): String? {
+ return executeRequest(null) {
+ params.contentScannerApi.getServerPublicKey()
+ }.publicKey
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
new file mode 100644
index 0000000000..dab9b5538f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.api.failure.toScanFailure
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ScanEncryptedTask : Task {
+ data class Params(
+ val mxcUrl: String,
+ val publicServerKey: String?,
+ val encryptedInfo: ElementToDecrypt
+ )
+}
+
+internal class DefaultScanEncryptedTask @Inject constructor(
+ private val contentScannerApiProvider: ContentScannerApiProvider,
+ private val contentScannerStore: ContentScannerStore
+) : ScanEncryptedTask {
+
+ override suspend fun execute(params: ScanEncryptedTask.Params): ScanResponse {
+ val mxcUrl = params.mxcUrl
+ val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(params.publicServerKey, params.mxcUrl, params.encryptedInfo)
+
+ val scannerUrl = contentScannerStore.getScannerUrl()
+ contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
+
+ try {
+ val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+ val executeRequest = executeRequest(null) {
+ api.scanFile(dlBody)
+ }
+ contentScannerStore.updateScanResultForContent(
+ mxcUrl,
+ scannerUrl,
+ ScanState.TRUSTED.takeIf { executeRequest.clean } ?: ScanState.INFECTED,
+ executeRequest.info ?: ""
+ )
+ return executeRequest
+ } catch (failure: Throwable) {
+ contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
+ throw failure.toScanFailure() ?: failure
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt
new file mode 100644
index 0000000000..505eb7098c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
+import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
+import org.matrix.android.sdk.api.failure.toScanFailure
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ScanMediaTask : Task {
+ data class Params(
+ val mxcUrl: String
+ )
+}
+
+internal class DefaultScanMediaTask @Inject constructor(
+ private val contentScannerApiProvider: ContentScannerApiProvider,
+ private val contentScannerStore: ContentScannerStore
+) : ScanMediaTask {
+
+ override suspend fun execute(params: ScanMediaTask.Params): ScanResponse {
+ // "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ"
+ if (!params.mxcUrl.isMxcUrl()) {
+ throw IllegalAccessException("Invalid mxc url")
+ }
+ val scannerUrl = contentScannerStore.getScannerUrl()
+ contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
+
+ var serverAndMediaId = params.mxcUrl.removeMxcPrefix()
+ val fragmentOffset = serverAndMediaId.indexOf("#")
+ if (fragmentOffset >= 0) {
+ serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
+ }
+
+ val split = serverAndMediaId.split("/")
+ if (split.size != 2) {
+ throw IllegalAccessException("Invalid mxc url")
+ }
+
+ try {
+ val scanResponse = executeRequest(null) {
+ val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+ api.scanMedia(split[0], split[1])
+ }
+ contentScannerStore.updateScanResultForContent(
+ params.mxcUrl,
+ scannerUrl,
+ ScanState.TRUSTED.takeIf { scanResponse.clean } ?: ScanState.INFECTED,
+ scanResponse.info ?: ""
+ )
+ return scanResponse
+ } catch (failure: Throwable) {
+ contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
+ throw failure.toScanFailure() ?: failure
+ }
+ }
+}
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
index 65974151c8..3e821b8956 100644
--- 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
@@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action
+import org.matrix.android.sdk.api.pushrules.PushEvents
import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleScope
@@ -142,79 +143,6 @@ internal class DefaultPushRuleService @Inject constructor(
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
}
-// fun processEvents(events: List) {
-// 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")
- }
- }
- }
- }
-
override fun getKeywords(): LiveData> {
// Keywords are all content rules that don't start with '.'
val liveData = monarchy.findAllMappedWithChanges(
@@ -229,4 +157,16 @@ internal class DefaultPushRuleService @Inject constructor(
results.firstOrNull().orEmpty().toSet()
}
}
+
+ fun dispatchEvents(pushEvents: PushEvents) {
+ synchronized(listeners) {
+ listeners.forEach {
+ try {
+ it.onEvents(pushEvents)
+ } catch (e: Throwable) {
+ Timber.e(e, "Error while dispatching push events")
+ }
+ }
+ }
+ }
}
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
index 3c74888eda..0ac21b555e 100644
--- 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
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.notification
+import org.matrix.android.sdk.api.pushrules.PushEvents
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isInvitation
@@ -39,14 +40,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
) : 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?.mapNotNull {
@@ -74,10 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
}
Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
" to check for push rules with ${params.rules.size} rules")
- allEvents.forEach { event ->
+ val matchedEvents = allEvents.mapNotNull { event ->
pushRuleFinder.fulfilledBingRule(event, params.rules)?.let {
Timber.v("[PushRules] Rule $it match for event ${event.eventId}")
- defaultPushRuleService.dispatchBing(event, it)
+ event to it
}
}
@@ -91,10 +84,13 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
- allRedactedEvents.forEach { redactedEventId ->
- defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
- }
-
- defaultPushRuleService.dispatchFinish()
+ defaultPushRuleService.dispatchEvents(
+ PushEvents(
+ matchedEvents = matchedEvents,
+ roomsJoined = params.syncResponse.join.keys,
+ roomsLeft = params.syncResponse.leave.keys,
+ redactedEventIds = allRedactedEvents
+ )
+ )
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
index 0e4493846c..d96beed3f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
@@ -43,7 +43,7 @@ internal class RoomAccountDataDataSource @Inject constructor(@SessionDatabase pr
fun getLiveAccountDataEvent(roomId: String, type: String): LiveData> {
return Transformations.map(getLiveAccountDataEvents(roomId, setOf(type))) {
- it.firstOrNull()?.toOptional()
+ it.firstOrNull().toOptional()
}
}
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
index 177c98541c..77aadef6bd 100644
--- 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
@@ -37,7 +37,6 @@ 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.api.session.room.model.message.MessageWithAttachmentContent
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.send.SendState
@@ -98,7 +97,7 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun sendPoll(question: String, options: List): Cancelable {
+ override fun sendPoll(question: String, options: List): Cancelable {
return localEchoEventFactory.createPollEvent(roomId, question, options)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
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
index 8dd0c59387..5cb9687518 100644
--- 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
@@ -38,13 +38,14 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF
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.MessagePollContent
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.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.PollAnswer
+import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
+import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
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
@@ -138,24 +139,29 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createPollEvent(roomId: String,
question: String,
- options: List): 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()
+ options: List): Event {
+ val content = MessagePollContent(
+ pollCreationInfo = PollCreationInfo(
+ question = PollQuestion(
+ question = question
+ ),
+ answers = options.mapIndexed { index, option ->
+ PollAnswer(
+ id = index.toString(),
+ answer = option
+ )
+ }
)
)
+ val localId = LocalEcho.createLocalEchoId()
+ return Event(
+ roomId = roomId,
+ originServerTs = dummyOriginServerTs(),
+ senderId = userId,
+ eventId = localId,
+ type = EventType.POLL_START,
+ content = content.toContent(),
+ unsignedData = UnsignedData(age = null, transactionId = localId))
}
fun createReplaceTextOfReply(roomId: String,
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
index 19f34746ab..7ac34e80e9 100644
--- 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
@@ -43,6 +43,7 @@ internal class DefaultSignOutTask @Inject constructor(
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) {
+ cleanupSession.stopActiveTasks()
Timber.d("SignOut: send request...")
try {
executeRequest(globalErrorReceiver) {
@@ -67,6 +68,6 @@ internal class DefaultSignOutTask @Inject constructor(
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
Timber.d("SignOut: cleanup session...")
- cleanupSession.handle()
+ cleanupSession.cleanup()
}
}
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
index 335f619623..8fd969e373 100644
--- 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
@@ -24,11 +24,13 @@ import org.matrix.android.sdk.api.session.initsync.InitSyncStep
import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
+import org.matrix.android.sdk.internal.SessionManager
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.SessionListeners
+import org.matrix.android.sdk.internal.session.dispatchTo
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@@ -51,6 +53,7 @@ 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 sessionManager: SessionManager,
private val sessionListeners: SessionListeners,
private val workManagerProvider: WorkManagerProvider,
private val roomSyncHandler: RoomSyncHandler,
@@ -158,8 +161,9 @@ internal class SyncResponseHandler @Inject constructor(
}
private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
+ val session = sessionManager.getSessionComponent(sessionId)?.session()
roomsSyncResponse.invite.keys.forEach { roomId ->
- sessionListeners.dispatch { session, listener ->
+ session.dispatchTo(sessionListeners) { session, listener ->
listener.onNewInvitedRoom(session, roomId)
}
}
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
index e1150f2c47..3faa0c9488 100644
--- 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
@@ -160,6 +160,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
synchronized(lock) { lock.wait() }
Timber.tag(loggerTag.value).d("...retry")
} else if (!isTokenValid) {
+ if (state == SyncState.Killing) {
+ continue
+ }
Timber.tag(loggerTag.value).d("Token is invalid. Waiting...")
updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
index b36bdc80f8..2c3d660333 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
@@ -41,7 +41,7 @@ internal class UserAccountDataDataSource @Inject constructor(@SessionDatabase pr
fun getLiveAccountDataEvent(type: String): LiveData> {
return Transformations.map(getLiveAccountDataEvents(setOf(type))) {
- it.firstOrNull()?.toOptional()
+ it.firstOrNull().toOptional()
}
}
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
index a12587ac56..3e977b31fb 100644
--- 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
@@ -16,9 +16,8 @@
package org.matrix.android.sdk.internal.util
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import org.matrix.android.sdk.internal.di.MatrixScope
import timber.log.Timber
import javax.inject.Inject
@@ -27,13 +26,12 @@ import javax.inject.Inject
* To be attached to ProcessLifecycleOwner lifecycle
*/
@MatrixScope
-internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
+internal class BackgroundDetectionObserver @Inject constructor() : DefaultLifecycleObserver {
var isInBackground: Boolean = true
private set
- private
- val listeners = LinkedHashSet()
+ private val listeners = LinkedHashSet()
fun register(listener: Listener) {
listeners.add(listener)
@@ -43,15 +41,13 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
listeners.remove(listener)
}
- @OnLifecycleEvent(Lifecycle.Event.ON_START)
- fun onMoveToForeground() {
+ override fun onStart(owner: LifecycleOwner) {
Timber.v("App returning to foreground…")
isInBackground = false
listeners.forEach { it.onMoveToForeground() }
}
- @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
- fun onMoveToBackground() {
+ override fun onStop(owner: LifecycleOwner) {
Timber.v("App going to background…")
isInBackground = true
listeners.forEach { it.onMoveToBackground() }
diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/ContactPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/ContactPicker.kt
index 315fe6cbf2..2b8c1d11e6 100644
--- a/multipicker/src/main/java/im/vector/lib/multipicker/ContactPicker.kt
+++ b/multipicker/src/main/java/im/vector/lib/multipicker/ContactPicker.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.content.Intent
import android.provider.ContactsContract
import im.vector.lib.multipicker.entity.MultiPickerContactType
+import im.vector.lib.multipicker.utils.getColumnIndexOrNull
/**
* Contact Picker implementation
@@ -49,9 +50,9 @@ class ContactPicker : Picker() {
null
)?.use { cursor ->
if (cursor.moveToFirst()) {
- val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
- val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
- val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI)
+ val idColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts._ID) ?: return@use
+ val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
+ val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
val contactId = cursor.getInt(idColumn)
var name = cursor.getString(nameColumn)
@@ -72,10 +73,13 @@ class ContactPicker : Picker() {
selection,
selectionArgs,
null
- )?.use { cursor ->
- while (cursor.moveToNext()) {
- val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
- val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
+ )?.use inner@{ innerCursor ->
+ val mimeTypeColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.MIMETYPE) ?: return@inner
+ val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
+
+ while (innerCursor.moveToNext()) {
+ val mimeType = innerCursor.getString(mimeTypeColumnIndex)
+ val contactData = innerCursor.getString(data1ColumnIndex)
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
name = contactData
@@ -115,7 +119,10 @@ class ContactPicker : Picker() {
selectionArgs,
null
)?.use { cursor ->
- return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null
+ return if (cursor.moveToFirst()) {
+ cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
+ ?.let { cursor.getInt(it) }
+ } else null
}
}
diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt
index ec98152aa7..8e6c97f2f8 100644
--- a/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt
+++ b/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt
@@ -21,6 +21,7 @@ import android.content.Intent
import android.provider.OpenableColumns
import im.vector.lib.multipicker.entity.MultiPickerBaseType
import im.vector.lib.multipicker.entity.MultiPickerFileType
+import im.vector.lib.multipicker.utils.getColumnIndexOrNull
import im.vector.lib.multipicker.utils.isMimeTypeAudio
import im.vector.lib.multipicker.utils.isMimeTypeImage
import im.vector.lib.multipicker.utils.isMimeTypeVideo
@@ -49,8 +50,8 @@ class FilePicker : Picker() {
// Other files
context.contentResolver.query(selectedUri, null, null, null, null)
?.use { cursor ->
- val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
+ val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
+ val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
if (cursor.moveToFirst()) {
val name = cursor.getString(nameColumn)
val size = cursor.getLong(sizeColumn)
diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt
index 78136c274a..55c0010afd 100644
--- a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt
+++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt
@@ -37,8 +37,8 @@ internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType?
null,
null
)?.use { cursor ->
- val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
- val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
+ val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.DISPLAY_NAME) ?: return@use null
+ val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getString(nameColumn)
@@ -75,8 +75,8 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
null,
null
)?.use { cursor ->
- val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
- val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE)
+ val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.DISPLAY_NAME) ?: return@use null
+ val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getString(nameColumn)
@@ -124,8 +124,8 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
null,
null
)?.use { cursor ->
- val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME)
- val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
+ val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.DISPLAY_NAME) ?: return@use null
+ val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
if (cursor.moveToNext()) {
val name = cursor.getString(nameColumn)
diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
new file mode 100644
index 0000000000..87cf48d0a7
--- /dev/null
+++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.lib.multipicker.utils
+
+import android.database.Cursor
+
+fun Cursor.getColumnIndexOrNull(column: String): Int? {
+ return getColumnIndex(column).takeIf { it != -1 }
+}
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index 29077c3a76..b135954f63 100644
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
-enum class===107
+enum class===108
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3
diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json
index 4f3038d53c..341cdc0c54 100644
--- a/tools/emojis/emoji_picker_datasource_formatted.json
+++ b/tools/emojis/emoji_picker_datasource_formatted.json
@@ -4191,7 +4191,8 @@
"call",
"hand",
"hands",
- "gesture"
+ "gesture",
+ "shaka"
]
},
"backhand-index-pointing-left": {
diff --git a/tools/release/sign_apk.sh b/tools/release/sign_apk.sh
index 7697f58ceb..aae9e1a378 100755
--- a/tools/release/sign_apk.sh
+++ b/tools/release/sign_apk.sh
@@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1
PARAM_APK=$2
# Other params
-BUILD_TOOLS_VERSION="30.0.3"
+BUILD_TOOLS_VERSION="31.0.0-rc5"
MIN_SDK_VERSION=21
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
diff --git a/tools/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh
index af5b0f0e32..5d209a4a2b 100755
--- a/tools/release/sign_apk_unsafe.sh
+++ b/tools/release/sign_apk_unsafe.sh
@@ -23,7 +23,7 @@ PARAM_KS_PASS=$3
PARAM_KEY_PASS=$4
# Other params
-BUILD_TOOLS_VERSION="30.0.3"
+BUILD_TOOLS_VERSION="31.0.0-rc5"
MIN_SDK_VERSION=21
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
diff --git a/vector/build.gradle b/vector/build.gradle
index 03b35465bf..655cff2e73 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -15,7 +15,7 @@ kapt {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 3
-ext.versionPatch = 7
+ext.versionPatch = 8
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -105,7 +105,6 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android {
-
// Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use
// Ref: https://issuetracker.google.com/issues/144111441
ndkVersion "21.3.6528147"
@@ -211,6 +210,7 @@ android {
// This property does not affect tests that you run using Android Studio.”
animationsDisabled = true
+ // Comment to run on Android 12
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
@@ -333,7 +333,6 @@ configurations {
dependencies {
implementation project(":matrix-sdk-android")
- implementation project(":matrix-sdk-android-rx")
implementation project(":matrix-sdk-android-flow")
implementation project(":diff-match-patch")
implementation project(":multipicker")
@@ -358,8 +357,10 @@ dependencies {
implementation libs.squareup.moshi
kapt libs.squareup.moshiKotlin
- implementation libs.androidx.lifecycleExtensions
+
+ // Lifecycle
implementation libs.androidx.lifecycleLivedata
+ implementation libs.androidx.lifecycleProcess
implementation libs.androidx.datastore
implementation libs.androidx.datastorepreferences
@@ -372,25 +373,18 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
- // rx
- implementation libs.rx.rxKotlin
- implementation libs.rx.rxAndroid
- implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1'
- // RXBinding
- implementation libs.jakewharton.rxbinding
- implementation libs.jakewharton.rxbindingAppcompat
- implementation libs.jakewharton.rxbindingMaterial
+ // FlowBinding
+ implementation libs.github.flowBinding
+ implementation libs.github.flowBindingAppcompat
+ implementation libs.github.flowBindingMaterial
implementation libs.airbnb.epoxy
implementation libs.airbnb.epoxyGlide
kapt libs.airbnb.epoxyProcessor
implementation libs.airbnb.epoxyPaging
implementation libs.airbnb.mavericks
- //TODO: remove when entirely migrated to Flow
- implementation libs.airbnb.mavericksRx
-
// Work
implementation libs.androidx.work
@@ -420,7 +414,7 @@ dependencies {
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
// Custom Tab
- implementation 'androidx.browser:browser:1.3.0'
+ implementation 'androidx.browser:browser:1.4.0'
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.5.2'
@@ -471,7 +465,7 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
implementation "androidx.emoji:emoji-appcompat:1.1.0"
- implementation ('com.github.BillCarsonFr:JsonViewer:0.7')
+ implementation('com.github.BillCarsonFr:JsonViewer:0.7')
// WebRTC
// org.webrtc:google-webrtc is for development purposes only
@@ -512,6 +506,9 @@ dependencies {
// Plant Timber tree for test
testImplementation libs.tests.timberJunitRule
testImplementation libs.airbnb.mavericksTesting
+ testImplementation(libs.jetbrains.coroutinesTest) {
+ exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
+ }
// Activate when you want to check for leaks, from time to time.
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
@@ -525,6 +522,9 @@ dependencies {
androidTestImplementation libs.androidx.espressoIntents
androidTestImplementation libs.tests.kluent
androidTestImplementation libs.androidx.coreTesting
+ androidTestImplementation(libs.jetbrains.coroutinesTest) {
+ exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
+ }
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso"
diff --git a/vector/lint.xml b/vector/lint.xml
index dde29af62e..e534bdc355 100644
--- a/vector/lint.xml
+++ b/vector/lint.xml
@@ -68,6 +68,7 @@
+
diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt
index 823ce83015..fbcb9b8cb3 100644
--- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt
+++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt
@@ -18,7 +18,10 @@ package im.vector.app
import android.app.Activity
import android.view.View
+import androidx.annotation.StringRes
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
+import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.PerformException
@@ -32,6 +35,12 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
+import com.adevinta.android.barista.interaction.BaristaClickInteractions
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.app.espresso.tools.waitUntilViewVisible
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.StringDescription
@@ -49,6 +58,18 @@ object EspressoHelper {
}
return currentActivity
}
+
+ inline fun > getBottomSheetDialog(): BottomSheetDialogFragment? {
+ return (getCurrentActivity() as? FragmentActivity)
+ ?.supportFragmentManager
+ ?.fragments
+ ?.filterIsInstance()
+ ?.firstOrNull()
+ }
+}
+
+fun getString(@StringRes id: Int): String {
+ return EspressoHelper.getCurrentActivity()!!.resources.getString(id)
}
fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
@@ -70,6 +91,8 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDispl
val endTime = startTime + timeout
val visibleMatcher = isDisplayed()
+ uiController.loopMainThreadForAtLeast(100)
+
do {
println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}")
val viewVisible = TreeIterables.breadthFirstViewTraversal(view)
@@ -205,3 +228,52 @@ fun allSecretsKnownIdling(session: Session): IdlingResource {
return res
}
+
+fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) {
+ BaristaClickInteractions.clickOn(name)
+ block()
+ Espresso.pressBack()
+}
+
+inline fun > interactWithSheet(contentMatcher: Matcher, noinline block: () -> Unit = {}) {
+ waitUntilViewVisible(contentMatcher)
+ val behaviour = (EspressoHelper.getBottomSheetDialog()!!.dialog as BottomSheetDialog).behavior
+ withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_EXPANDED), block)
+ withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_HIDDEN)) {}
+}
+
+class BottomSheetResource(
+ private val bottomSheetBehavior: BottomSheetBehavior<*>,
+ @BottomSheetBehavior.State private val wantedState: Int
+) : IdlingResource, BottomSheetBehavior.BottomSheetCallback() {
+
+ private var isIdle: Boolean = false
+ private var resourceCallback: IdlingResource.ResourceCallback? = null
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ val wasIdle = isIdle
+ isIdle = newState == BottomSheetBehavior.STATE_EXPANDED
+ if (!wasIdle && isIdle) {
+ bottomSheetBehavior.removeBottomSheetCallback(this)
+ resourceCallback?.onTransitionToIdle()
+ }
+ }
+
+ override fun getName() = "BottomSheet awaiting state: $wantedState"
+
+ override fun isIdleNow() = isIdle
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
+ resourceCallback = callback
+
+ val state = bottomSheetBehavior.state
+ isIdle = state == wantedState
+ if (isIdle) {
+ resourceCallback!!.onTransitionToIdle()
+ } else {
+ bottomSheetBehavior.addBottomSheetCallback(this)
+ }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt
index a562287263..47e1e43be3 100644
--- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt
+++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt
@@ -18,7 +18,7 @@ package im.vector.app
import android.net.Uri
import androidx.lifecycle.Observer
-import im.vector.app.ui.UiTestBase
+import im.vector.app.ui.robot.OnboardingRobot
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -39,7 +39,7 @@ abstract class VerificationTestBase {
val password = "password"
val homeServerUrl: String = "http://10.0.2.2:8080"
- protected val uiTestBase = UiTestBase()
+ protected val uiTestBase = OnboardingRobot()
fun createAccountAndSync(matrix: Matrix,
userName: String,
diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt
new file mode 100644
index 0000000000..2e329ebb6b
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.espresso.tools
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import timber.log.Timber
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStream
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+private val SCREENSHOT_FOLDER_LOCATION = "${Environment.DIRECTORY_PICTURES}/failure_screenshots"
+private val deviceLanguage = Locale.getDefault().language
+
+class ScreenshotFailureRule : TestWatcher() {
+ override fun failed(e: Throwable?, description: Description) {
+ val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
+ val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
+ storeFailureScreenshot(bitmap, screenShotName)
+ }
+}
+
+/**
+ * Stores screenshots in sdcard/Pictures/failure_screenshots
+ */
+private fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) {
+ val contentResolver = getInstrumentation().targetContext.applicationContext.contentResolver
+
+ val contentValues = ContentValues().apply {
+ put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
+ put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ useMediaStoreScreenshotStorage(
+ contentValues,
+ contentResolver,
+ screenshotName,
+ SCREENSHOT_FOLDER_LOCATION,
+ bitmap
+ )
+ } else {
+ usePublicExternalScreenshotStorage(
+ contentValues,
+ contentResolver,
+ screenshotName,
+ SCREENSHOT_FOLDER_LOCATION,
+ bitmap
+ )
+ }
+}
+
+private fun useMediaStoreScreenshotStorage(
+ contentValues: ContentValues,
+ contentResolver: ContentResolver,
+ screenshotName: String,
+ screenshotLocation: String,
+ bitmap: Bitmap
+) {
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "$screenshotName.jpeg")
+ contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, screenshotLocation)
+ val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ if (uri != null) {
+ contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) }
+ contentResolver.update(uri, contentValues, null, null)
+ }
+}
+
+@Suppress("DEPRECATION")
+private fun usePublicExternalScreenshotStorage(
+ contentValues: ContentValues,
+ contentResolver: ContentResolver,
+ screenshotName: String,
+ screenshotLocation: String,
+ bitmap: Bitmap
+) {
+ val directory = File(Environment.getExternalStoragePublicDirectory(screenshotLocation).toString())
+ if (!directory.exists()) {
+ directory.mkdirs()
+ }
+ val file = File(directory, "$screenshotName.jpeg")
+ saveScreenshotToStream(bitmap, FileOutputStream(file))
+ contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+}
+
+private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) {
+ outputStream.use {
+ try {
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 50, it)
+ } catch (e: IOException) {
+ Timber.e("Screenshot was not stored at this time")
+ }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt
index 2cdca62c74..7744d4b720 100644
--- a/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt
+++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/WaitActivity.kt
@@ -17,9 +17,18 @@
package im.vector.app.espresso.tools
import android.app.Activity
+import android.view.View
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.activityIdlingResource
+import im.vector.app.waitForView
import im.vector.app.withIdlingResource
+import org.hamcrest.Matcher
-inline fun waitUntilActivityVisible(noinline block: (() -> Unit)) {
+inline fun waitUntilActivityVisible(noinline block: (() -> Unit) = {}) {
withIdlingResource(activityIdlingResource(T::class.java), block)
}
+
+fun waitUntilViewVisible(viewMatcher: Matcher) {
+ Espresso.onView(ViewMatchers.isRoot()).perform(waitForView(viewMatcher))
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
index a9cb5274ed..f998a9f23c 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
@@ -16,53 +16,19 @@
package im.vector.app.ui
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.Espresso.pressBack
-import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
-import androidx.test.espresso.action.ViewActions.longClick
-import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
-import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.isRoot
-import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import com.adevinta.android.barista.assertion.BaristaListAssertions.assertListItemCount
-import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
-import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickBack
-import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
-import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn
-import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
-import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
-import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawer
-import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
-import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
-import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItemChild
-import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
-import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
-import im.vector.app.BuildConfig
-import im.vector.app.EspressoHelper
import im.vector.app.R
-import im.vector.app.SleepViewAction
-import im.vector.app.activityIdlingResource
-import im.vector.app.espresso.tools.clickOnPreference
-import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
-import im.vector.app.features.createdirect.CreateDirectRoomActivity
-import im.vector.app.features.home.HomeActivity
-import im.vector.app.features.home.room.detail.RoomDetailActivity
-import im.vector.app.features.login.LoginActivity
-import im.vector.app.features.roomdirectory.RoomDirectoryActivity
-import im.vector.app.initialSyncIdlingResource
-import im.vector.app.waitForView
-import im.vector.app.withIdlingResource
+import im.vector.app.getString
+import im.vector.app.ui.robot.ElementRobot
+import im.vector.app.ui.robot.withDeveloperMode
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.lang.Thread.sleep
import java.util.UUID
/**
@@ -73,9 +39,11 @@ import java.util.UUID
class UiAllScreensSanityTest {
@get:Rule
- val activityRule = ActivityScenarioRule(MainActivity::class.java)
+ val testRule = RuleChain
+ .outerRule(ActivityScenarioRule(MainActivity::class.java))
+ .around(ScreenshotFailureRule())
- private val uiTestBase = UiTestBase()
+ private val elementRobot = ElementRobot()
// Last passing:
// 2020-11-09
@@ -85,439 +53,63 @@ class UiAllScreensSanityTest {
fun allScreensTest() {
// Create an account
val userId = "UiTest_" + UUID.randomUUID().toString()
- uiTestBase.createAccount(userId = userId)
+ elementRobot.signUp(userId)
- withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
- assertDisplayed(R.id.roomListContainer)
- closeSoftKeyboard()
+ elementRobot.settings {
+ general { crawl() }
+ notifications { crawl() }
+ preferences { crawl() }
+ voiceAndVideo()
+ ignoredUsers()
+ securityAndPrivacy { crawl() }
+ labs()
+ advancedSettings { crawl() }
+ helpAndAbout { crawl() }
}
- val activity = EspressoHelper.getCurrentActivity()!!
- val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
-
- withIdlingResource(initialSyncIdlingResource(uiSession)) {
- assertDisplayed(R.id.roomListContainer)
+ elementRobot.newDirectMessage {
+ verifyQrCodeButton()
+ verifyInviteFriendsButton()
}
- assertDisplayed(R.id.bottomNavigationView)
+ elementRobot.newRoom {
+ createNewRoom {
+ crawl()
+ createRoom {
+ val message = "Hello world!"
+ postMessage(message)
+ crawl()
+ crawlMessage(message)
+ openSettings { crawl() }
+ }
+ }
+ }
- // Settings
- navigateToSettings()
+ elementRobot.withDeveloperMode {
+ settings {
+ advancedSettings { crawlDeveloperOptions() }
+ }
+ roomList {
+ openRoom(getString(R.string.room_displayname_empty_room)) {
+ val message = "Test view source"
+ postMessage(message)
+ openMessageMenu(message) {
+ viewSource()
+ }
+ }
+ }
+ }
- // Create DM
- clickOn(R.id.bottom_action_people)
- createDm()
+ elementRobot.roomList {
+ verifyCreatedRoom()
+ }
- // Create Room
- // First navigate to the other tab
- clickOn(R.id.bottom_action_rooms)
- createRoom()
-
- assertDisplayed(R.id.bottomNavigationView)
-
- // Long click on the room
- onView(withId(R.id.roomListView))
- .perform(
- actionOnItem(
- hasDescendant(withText(R.string.room_displayname_empty_room)),
- longClick()
- )
- )
- pressBack()
-
- uiTestBase.signout()
-
- // We have sent a message in a e2e room, accept to loose it
- clickOn(R.id.exitAnywayButton)
- // Dark pattern
- clickDialogNegativeButton()
+ elementRobot.signout(expectSignOutWarning = true)
// Login again on the same account
- waitUntilActivityVisible {
- assertDisplayed(R.id.loginSplashLogo)
- }
-
- uiTestBase.login(userId)
- ignoreVerification()
-
- uiTestBase.signout()
- clickDialogPositiveButton()
-
+ elementRobot.login(userId)
+ elementRobot.dismissVerificationIfPresent()
// TODO Deactivate account instead of logout?
- }
-
- private fun ignoreVerification() {
- sleep(6000)
- val activity = EspressoHelper.getCurrentActivity()!!
-
- val popup = activity.findViewById(com.tapadoo.alerter.R.id.llAlertBackground)
- activity.runOnUiThread {
- popup.performClick()
- }
-
- assertDisplayed(R.id.bottomSheetFragmentContainer)
-
- onView(isRoot()).perform(SleepViewAction.sleep(2000))
-
- clickOn(R.string.skip)
- assertDisplayed(R.string.are_you_sure)
- clickOn(R.string.skip)
- }
-
- private fun createRoom() {
- clickOn(R.id.createGroupRoomButton)
- waitUntilActivityVisible {
- assertDisplayed(R.id.publicRoomsList)
- }
- clickOn(R.string.create_new_room)
-
- // Room access bottom sheet
- clickOn(R.string.room_settings_room_access_private_title)
- pressBack()
-
- // Create
- assertListItemCount(R.id.createRoomForm, 12)
- clickListItemChild(R.id.createRoomForm, 11, R.id.form_submit_button)
-
- waitUntilActivityVisible {
- assertDisplayed(R.id.roomDetailContainer)
- }
-
- clickOn(R.id.attachmentButton)
- clickBack()
-
- // Send a message
- writeTo(R.id.composerEditText, "Hello world!")
- clickOn(R.id.sendButton)
-
- navigateToRoomSettings()
-
- // Long click on the message
- longClickOnMessageTest()
-
- // Menu
- openMenu()
- pressBack()
- clickMenu(R.id.voice_call)
- pressBack()
- clickMenu(R.id.video_call)
- pressBack()
- clickMenu(R.id.search)
- pressBack()
-
- pressBack()
- }
-
- private fun longClickOnMessageTest() {
- // Test quick reaction
- longClickOnMessage()
- // Add quick reaction
- clickOn("\uD83D\uDC4D️") // 👍
-
- sleep(1000)
-
- // Open reactions
- longClickOn("\uD83D\uDC4D️") // 👍
- pressBack()
-
- // Test add reaction
- longClickOnMessage()
- clickOn(R.string.message_add_reaction)
- // Filter
- // TODO clickMenu(R.id.search)
- // Wait for emoji to load, it's async now
- sleep(2_000)
- clickListItem(R.id.emojiRecyclerView, 4)
-
- // Test Edit mode
- longClickOnMessage()
- clickOn(R.string.edit)
- // TODO Cancel action
- writeTo(R.id.composerEditText, "Hello universe!")
- // Wait a bit for the keyboard layout to update
- sleep(30)
- clickOn(R.id.sendButton)
- // Wait for the UI to update
- sleep(1000)
- // Open edit history
- longClickOnMessage("Hello universe! (edited)")
- clickOn(R.string.message_view_edit_history)
- pressBack()
- }
-
- private fun longClickOnMessage(text: String = "Hello world!") {
- onView(withId(R.id.timelineRecyclerView))
- .perform(
- actionOnItem(
- hasDescendant(withText(text)),
- longClick()
- )
- )
- }
-
- private fun navigateToRoomSettings() {
- clickOn(R.id.roomToolbarTitleView)
- assertDisplayed(R.id.roomProfileAvatarView)
-
- // Room settings
- clickListItem(R.id.matrixProfileRecyclerView, 3)
- navigateToRoomParameters()
- pressBack()
-
- // Notifications
- clickListItem(R.id.matrixProfileRecyclerView, 5)
- pressBack()
-
- assertDisplayed(R.id.roomProfileAvatarView)
-
- // People
- clickListItem(R.id.matrixProfileRecyclerView, 7)
- assertDisplayed(R.id.inviteUsersButton)
- navigateToRoomPeople()
- // Fab
- navigateToInvite()
- pressBack()
- pressBack()
-
- assertDisplayed(R.id.roomProfileAvatarView)
-
- // Uploads
- clickListItem(R.id.matrixProfileRecyclerView, 9)
- // File tab
- clickOn(R.string.uploads_files_title)
- sleep(1000)
- pressBack()
-
- assertDisplayed(R.id.roomProfileAvatarView)
-
- // Leave
- clickListItem(R.id.matrixProfileRecyclerView, 13)
- clickDialogNegativeButton()
-
- // Advanced
- // Room addresses
- clickListItem(R.id.matrixProfileRecyclerView, 15)
- onView(isRoot()).perform(waitForView(withText(R.string.room_alias_published_alias_title)))
- pressBack()
-
- // Room permissions
- clickListItem(R.id.matrixProfileRecyclerView, 17)
- onView(isRoot()).perform(waitForView(withText(R.string.room_permissions_title)))
- clickOn(R.string.room_permissions_change_room_avatar)
- clickDialogNegativeButton()
- // Toggle
- clickOn(R.string.show_advanced)
- clickOn(R.string.hide_advanced)
- pressBack()
-
- // Menu share
- // clickMenu(R.id.roomProfileShareAction)
- // pressBack()
-
- pressBack()
- }
-
- private fun navigateToRoomParameters() {
- // Room history readability
- clickListItem(R.id.roomSettingsRecyclerView, 4)
- pressBack()
-
- // Room access
- clickListItem(R.id.roomSettingsRecyclerView, 6)
- pressBack()
- }
-
- private fun navigateToInvite() {
- assertDisplayed(R.id.inviteUsersButton)
- clickOn(R.id.inviteUsersButton)
- closeSoftKeyboard()
- pressBack()
- }
-
- private fun navigateToRoomPeople() {
- // Open first user
- clickListItem(R.id.roomSettingsRecyclerView, 1)
- sleep(1000)
- assertDisplayed(R.id.memberProfilePowerLevelView)
-
- // Verification
- clickListItem(R.id.matrixProfileRecyclerView, 1)
- clickBack()
-
- // Role
- clickListItem(R.id.matrixProfileRecyclerView, 3)
- sleep(1000)
- clickDialogNegativeButton()
- sleep(1000)
- clickBack()
- }
-
- private fun createDm() {
- clickOn(R.id.createChatRoomButton)
-
- withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
- onView(withId(R.id.userListRecyclerView))
- .perform(waitForView(withText(R.string.qr_code)))
- onView(withId(R.id.userListRecyclerView))
- .perform(waitForView(withText(R.string.invite_friends)))
- }
-
- closeSoftKeyboard()
- pressBack()
- pressBack()
- }
-
- private fun navigateToSettings() {
- // clickOn(R.id.groupToolbarAvatarImageView)
- openDrawer()
- clickOn(R.id.homeDrawerHeaderSettingsView)
-
- clickOn(R.string.settings_general_title)
- navigateToSettingsGeneral()
- pressBack()
-
- clickOn(R.string.settings_notifications)
- navigateToSettingsNotifications()
- pressBack()
-
- clickOn(R.string.settings_preferences)
- navigateToSettingsPreferences()
- pressBack()
-
- clickOn(R.string.preference_voice_and_video)
- pressBack()
-
- clickOn(R.string.settings_ignored_users)
- pressBack()
-
- clickOn(R.string.settings_security_and_privacy)
- navigateToSettingsSecurity()
- pressBack()
-
- clickOn(R.string.room_settings_labs_pref_title)
- pressBack()
-
- clickOn(R.string.settings_advanced_settings)
- navigateToSettingsAdvanced()
- pressBack()
-
- clickOn(R.string.preference_root_help_about)
- navigateToSettingsHelp()
- pressBack()
-
- pressBack()
- }
-
- private fun navigateToSettingsHelp() {
- /*
- clickOn(R.string.settings_app_info_link_title)
- Cannot go back...
- pressBack()
- clickOn(R.string.settings_copyright)
- pressBack()
- clickOn(R.string.settings_app_term_conditions)
- pressBack()
- clickOn(R.string.settings_privacy_policy)
- pressBack()
- */
- clickOn(R.string.settings_third_party_notices)
- clickDialogPositiveButton()
- }
-
- private fun navigateToSettingsAdvanced() {
- clickOnPreference(R.string.settings_notifications_targets)
- pressBack()
-
- clickOnPreference(R.string.settings_push_rules)
- pressBack()
-
- /* TODO P2 test developer screens
- // Enable developer mode
- clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
-
- clickOnPreference(R.string.settings_account_data)
- clickOn("m.push_rules")
- pressBack()
- pressBack()
- clickOnPreference(R.string.settings_key_requests)
- pressBack()
-
- // Disable developer mode
- clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
- */
- }
-
- private fun navigateToSettingsSecurity() {
- clickOnPreference(R.string.settings_active_sessions_show_all)
- pressBack()
-
- clickOnPreference(R.string.encryption_message_recovery)
- // TODO go deeper here
- pressBack()
- /* Cannot exit
- clickOnPreference(R.string.encryption_export_e2e_room_keys)
- pressBack()
- */
- }
-
- private fun navigateToSettingsPreferences() {
- clickOn(R.string.settings_interface_language)
- onView(isRoot())
- .perform(waitForView(withText("Dansk (Danmark)")))
- pressBack()
- clickOn(R.string.settings_theme)
- clickDialogNegativeButton()
- clickOn(R.string.font_size)
- clickDialogNegativeButton()
- }
-
- private fun navigateToSettingsNotifications() {
- if (BuildConfig.USE_NOTIFICATION_SETTINGS_V2) {
- clickOn(R.string.settings_notification_default)
- pressBack()
- clickOn(R.string.settings_notification_mentions_and_keywords)
- // TODO Test adding a keyword?
- pressBack()
- clickOn(R.string.settings_notification_other)
- pressBack()
- } else {
- clickOn(R.string.settings_notification_advanced)
- pressBack()
- }
- /*
- clickOn(R.string.settings_noisy_notifications_preferences)
- TODO Cannot go back
- pressBack()
- clickOn(R.string.settings_silent_notifications_preferences)
- pressBack()
- clickOn(R.string.settings_call_notifications_preferences)
- pressBack()
- */
- clickOnPreference(R.string.settings_notification_troubleshoot)
- pressBack()
- }
-
- private fun navigateToSettingsGeneral() {
- clickOn(R.string.settings_profile_picture)
- clickDialogPositiveButton()
- clickOn(R.string.settings_display_name)
- clickDialogNegativeButton()
- clickOn(R.string.settings_password)
- clickDialogNegativeButton()
- clickOn(R.string.settings_emails_and_phone_numbers_title)
- pressBack()
- clickOn(R.string.settings_discovery_manage)
- clickOn(R.string.add_identity_server)
- pressBack()
- pressBack()
- // Homeserver
- clickOnPreference(R.string.settings_home_server)
- pressBack()
- // Identity server
- clickOnPreference(R.string.settings_identity_server)
- pressBack()
- // Deactivate account
- clickOnPreference(R.string.settings_deactivate_my_account)
- pressBack()
+ elementRobot.signout(expectSignOutWarning = false)
}
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/CreateNewRoomRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/CreateNewRoomRobot.kt
new file mode 100644
index 0000000000..505dfb33e9
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/CreateNewRoomRobot.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.adevinta.android.barista.assertion.BaristaListAssertions
+import com.adevinta.android.barista.interaction.BaristaClickInteractions
+import com.adevinta.android.barista.interaction.BaristaListInteractions
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.home.room.detail.RoomDetailActivity
+
+class CreateNewRoomRobot(
+ var createdRoom: Boolean = false
+) {
+
+ fun createRoom(block: RoomDetailRobot.() -> Unit) {
+ createdRoom = true
+ BaristaListAssertions.assertListItemCount(R.id.createRoomForm, 12)
+ BaristaListInteractions.clickListItemChild(R.id.createRoomForm, 11, R.id.form_submit_button)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.composerEditText))
+ }
+ block(RoomDetailRobot())
+ pressBack()
+ }
+
+ fun crawl() {
+ // Room access bottom sheet
+ BaristaClickInteractions.clickOn(R.string.room_settings_room_access_private_title)
+ pressBack()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/TelecomUtils.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/DialogRobot.kt
similarity index 55%
rename from vector/src/main/java/im/vector/app/features/call/telecom/TelecomUtils.kt
rename to vector/src/androidTest/java/im/vector/app/ui/robot/DialogRobot.kt
index 819a1f6c0a..14c2d6284e 100644
--- a/vector/src/main/java/im/vector/app/features/call/telecom/TelecomUtils.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/DialogRobot.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020 New Vector Ltd
+ * Copyright (c) 2021 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.
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-package im.vector.app.features.call.telecom
+package im.vector.app.ui.robot
-import android.content.Context
-import android.telephony.TelephonyManager
-import androidx.core.content.getSystemService
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
-object TelecomUtils {
+class DialogRobot(
+ var returnedToPreviousScreen: Boolean = false
+) {
- fun isLineBusy(context: Context): Boolean {
- val telephonyManager = context.getSystemService()
- ?: return false
- return telephonyManager.callState != TelephonyManager.CALL_STATE_IDLE
+ fun negativeAction() {
+ clickDialogNegativeButton()
+ returnedToPreviousScreen = true
}
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
new file mode 100644
index 0000000000..a3bc5b26fc
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import android.view.View
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
+import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawer
+import im.vector.app.EspressoHelper
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.createdirect.CreateDirectRoomActivity
+import im.vector.app.features.home.HomeActivity
+import im.vector.app.features.login.LoginActivity
+import im.vector.app.initialSyncIdlingResource
+import im.vector.app.ui.robot.settings.SettingsRobot
+import im.vector.app.withIdlingResource
+import timber.log.Timber
+
+class ElementRobot {
+
+ fun signUp(userId: String) {
+ val onboardingRobot = OnboardingRobot()
+ onboardingRobot.createAccount(userId = userId)
+ waitForHome()
+ }
+
+ fun login(userId: String) {
+ val onboardingRobot = OnboardingRobot()
+ onboardingRobot.login(userId = userId)
+ waitForHome()
+ }
+
+ private fun waitForHome() {
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.roomListContainer))
+ }
+ val activity = EspressoHelper.getCurrentActivity()!!
+ val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
+ withIdlingResource(initialSyncIdlingResource(uiSession)) {
+ waitUntilViewVisible(withId(R.id.bottomNavigationView))
+ }
+ }
+
+ fun settings(block: SettingsRobot.() -> Unit) {
+ openDrawer()
+ clickOn(R.id.homeDrawerHeaderSettingsView)
+ block(SettingsRobot())
+ pressBack()
+ waitUntilViewVisible(withId(R.id.bottomNavigationView))
+ }
+
+ fun newDirectMessage(block: NewDirectMessageRobot.() -> Unit) {
+ clickOn(R.id.bottom_action_people)
+ clickOn(R.id.createChatRoomButton)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.userListSearch))
+ }
+ // close keyboard
+ pressBack()
+ block(NewDirectMessageRobot())
+ pressBack()
+ waitUntilViewVisible(withId(R.id.bottomNavigationView))
+ }
+
+ fun newRoom(block: NewRoomRobot.() -> Unit) {
+ clickOn(R.id.bottom_action_rooms)
+ RoomListRobot().newRoom { block() }
+ waitUntilViewVisible(withId(R.id.bottomNavigationView))
+ }
+
+ fun roomList(block: RoomListRobot.() -> Unit) {
+ clickOn(R.id.bottom_action_rooms)
+ block(RoomListRobot())
+ waitUntilViewVisible(withId(R.id.bottomNavigationView))
+ }
+
+ fun signout(expectSignOutWarning: Boolean) {
+ clickOn(R.id.groupToolbarAvatarImageView)
+ clickOn(R.id.homeDrawerHeaderSignoutView)
+
+ val isShowingSignOutWarning = kotlin.runCatching {
+ waitUntilViewVisible(withId(R.id.exitAnywayButton))
+ }.isSuccess
+
+ if (expectSignOutWarning != isShowingSignOutWarning) {
+ Timber.w("Unexpected sign out flow, expected warning to be: ${expectSignOutWarning.toWarningType()} but was ${isShowingSignOutWarning.toWarningType()}")
+ }
+
+ if (isShowingSignOutWarning) {
+ // We have sent a message in a e2e room, accept to loose it
+ clickOn(R.id.exitAnywayButton)
+ // Dark pattern
+ waitUntilViewVisible(withId(android.R.id.button2))
+ clickDialogNegativeButton()
+ } else {
+ waitUntilViewVisible(withId(android.R.id.button1))
+ clickDialogPositiveButton()
+ }
+
+ waitUntilActivityVisible {
+ assertDisplayed(R.id.loginSplashLogo)
+ }
+ }
+
+ fun dismissVerificationIfPresent() {
+ kotlin.runCatching {
+ Thread.sleep(6000)
+ val activity = EspressoHelper.getCurrentActivity()!!
+ val popup = activity.findViewById(com.tapadoo.alerter.R.id.llAlertBackground)!!
+ activity.runOnUiThread { popup.performClick() }
+
+ waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
+ waitUntilViewVisible(ViewMatchers.withText(R.string.skip))
+ clickOn(R.string.skip)
+ assertDisplayed(R.string.are_you_sure)
+ clickOn(R.string.skip)
+ waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
+ }.onFailure { Timber.w("Verification popup missing", it) }
+ }
+}
+
+private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
+
+fun ElementRobot.withDeveloperMode(block: ElementRobot.() -> Unit) {
+ settings { toggleDeveloperMode() }
+ block()
+ settings { toggleDeveloperMode() }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
new file mode 100644
index 0000000000..fd579c0d9f
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.test.espresso.Espresso.pressBack
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
+import im.vector.app.R
+import java.lang.Thread.sleep
+
+class MessageMenuRobot(
+ var autoClosed: Boolean = false
+) {
+
+ fun viewSource() {
+ clickOn(R.string.view_source)
+ // wait for library
+ sleep(1000)
+ pressBack()
+ autoClosed = true
+ }
+
+ fun editHistory() {
+ clickOn(R.string.message_view_edit_history)
+ pressBack()
+ autoClosed = true
+ }
+
+ fun addQuickReaction(quickReaction: String) {
+ clickOn(quickReaction)
+ autoClosed = true
+ }
+
+ fun addReactionFromEmojiPicker() {
+ clickOn(R.string.message_add_reaction)
+ // Wait for emoji to load, it's async now
+ sleep(2000)
+ clickListItem(R.id.emojiRecyclerView, 4)
+ autoClosed = true
+ }
+
+ fun edit() {
+ clickOn(R.string.edit)
+ autoClosed = true
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/NewDirectMessageRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/NewDirectMessageRobot.kt
new file mode 100644
index 0000000000..34c43c73f7
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/NewDirectMessageRobot.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.matcher.ViewMatchers
+import im.vector.app.R
+import im.vector.app.waitForView
+
+class NewDirectMessageRobot {
+
+ fun verifyQrCodeButton() {
+ Espresso.onView(ViewMatchers.withId(R.id.userListRecyclerView))
+ .perform(waitForView(ViewMatchers.withText(R.string.qr_code)))
+ }
+
+ fun verifyInviteFriendsButton() {
+ Espresso.onView(ViewMatchers.withId(R.id.userListRecyclerView))
+ .perform(waitForView(ViewMatchers.withText(R.string.invite_friends)))
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/NewRoomRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/NewRoomRobot.kt
new file mode 100644
index 0000000000..09ff1162c0
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/NewRoomRobot.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilViewVisible
+
+class NewRoomRobot(
+ var createdRoom: Boolean = false
+) {
+
+ fun createNewRoom(block: CreateNewRoomRobot.() -> Unit) {
+ clickOn(R.string.create_new_room)
+ waitUntilViewVisible(withId(R.id.createRoomForm))
+ val createNewRoomRobot = CreateNewRoomRobot()
+ block(createNewRoomRobot)
+ createdRoom = createNewRoomRobot.createdRoom
+ if (!createNewRoomRobot.createdRoom) {
+ pressBack()
+ }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
similarity index 88%
rename from vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt
rename to vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
index 2cef326501..fef5d4a1a2 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.app.ui
+package im.vector.app.ui.robot
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
@@ -26,11 +26,10 @@ import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assert
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
import im.vector.app.R
-import im.vector.app.espresso.tools.waitUntilActivityVisible
-import im.vector.app.features.home.HomeActivity
import im.vector.app.waitForView
-class UiTestBase {
+class OnboardingRobot {
+
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl)
}
@@ -76,15 +75,5 @@ class UiTestBase {
closeSoftKeyboard()
clickOn(R.id.loginSubmit)
-
- // Wait
- waitUntilActivityVisible {
- assertDisplayed(R.id.homeDetailFragmentContainer)
- }
- }
-
- fun signout() {
- clickOn(R.id.groupToolbarAvatarImageView)
- clickOn(R.id.homeDrawerHeaderSignoutView)
}
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
new file mode 100644
index 0000000000..24fe5adf64
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.interaction.BaristaClickInteractions
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn
+import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
+import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
+import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
+import im.vector.app.features.reactions.data.EmojiDataSource
+import im.vector.app.interactWithSheet
+import im.vector.app.waitForView
+import java.lang.Thread.sleep
+
+class RoomDetailRobot {
+
+ fun postMessage(content: String) {
+ writeTo(R.id.composerEditText, content)
+ waitUntilViewVisible(withId(R.id.sendButton))
+ clickOn(R.id.sendButton)
+ waitUntilViewVisible(withText(content))
+ }
+
+ fun crawl() {
+ clickOn(R.id.attachmentButton)
+ BaristaClickInteractions.clickBack()
+
+ // Menu
+ openMenu()
+ pressBack()
+ clickMenu(R.id.voice_call)
+ pressBack()
+ clickMenu(R.id.video_call)
+ pressBack()
+ clickMenu(R.id.search)
+ pressBack()
+ }
+
+ fun crawlMessage(message: String) {
+ // Test quick reaction
+ val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
+ openMessageMenu(message) {
+ addQuickReaction(quickReaction)
+ }
+ // Open reactions
+ longClickOn(quickReaction)
+ // wait for bottom sheet
+ pressBack()
+ // Test add reaction
+ openMessageMenu(message) {
+ addReactionFromEmojiPicker()
+ }
+ // Test Edit mode
+ openMessageMenu(message) {
+ edit()
+ }
+ // TODO Cancel action
+ writeTo(R.id.composerEditText, "Hello universe!")
+ // Wait a bit for the keyboard layout to update
+ waitUntilViewVisible(withId(R.id.sendButton))
+ clickOn(R.id.sendButton)
+ // Wait for the UI to update
+ waitUntilViewVisible(withText("Hello universe! (edited)"))
+ // Open edit history
+ openMessageMenu("Hello universe! (edited)") {
+ editHistory()
+ }
+ }
+
+ fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) {
+ onView(withId(R.id.timelineRecyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
+ ViewActions.longClick()
+ )
+ )
+ interactWithSheet(contentMatcher = withId(R.id.bottomSheetRecyclerView)) {
+ val messageMenuRobot = MessageMenuRobot()
+ block(messageMenuRobot)
+ if (!messageMenuRobot.autoClosed) {
+ pressBack()
+ }
+ }
+ }
+
+ fun openSettings(block: RoomSettingsRobot.() -> Unit) {
+ clickOn(R.id.roomToolbarTitleView)
+ waitForView(withId(R.id.roomProfileAvatarView))
+ sleep(1000)
+ block(RoomSettingsRobot())
+ pressBack()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt
new file mode 100644
index 0000000000..dc07f06202
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.features.roomdirectory.RoomDirectoryActivity
+
+class RoomListRobot {
+
+ fun openRoom(roomName: String, block: RoomDetailRobot.() -> Unit) {
+ clickOn(roomName)
+ block(RoomDetailRobot())
+ pressBack()
+ }
+
+ fun verifyCreatedRoom() {
+ onView(ViewMatchers.withId(R.id.roomListView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)),
+ ViewActions.longClick()
+ )
+ )
+ pressBack()
+ }
+
+ fun newRoom(block: NewRoomRobot.() -> Unit) {
+ clickOn(R.id.createGroupRoomButton)
+ waitUntilActivityVisible {
+ BaristaVisibilityAssertions.assertDisplayed(R.id.publicRoomsList)
+ }
+ val newRoomRobot = NewRoomRobot()
+ block(newRoomRobot)
+ if (!newRoomRobot.createdRoom) {
+ pressBack()
+ }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt
new file mode 100644
index 0000000000..15186fe0c8
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot
+
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
+import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity
+
+class RoomSettingsRobot {
+
+ fun crawl() {
+ // Room settings
+ clickListItem(R.id.matrixProfileRecyclerView, 3)
+ navigateToRoomParameters()
+ pressBack()
+
+ // Notifications
+ clickListItem(R.id.matrixProfileRecyclerView, 5)
+ pressBack()
+
+ assertDisplayed(R.id.roomProfileAvatarView)
+
+ // People
+ clickListItem(R.id.matrixProfileRecyclerView, 7)
+ assertDisplayed(R.id.inviteUsersButton)
+ navigateToRoomPeople()
+ // Fab
+ navigateToInvite()
+ pressBack()
+ pressBack()
+
+ assertDisplayed(R.id.roomProfileAvatarView)
+
+ // Uploads
+ clickListItem(R.id.matrixProfileRecyclerView, 9)
+ // File tab
+ clickOn(R.string.uploads_files_title)
+ waitUntilViewVisible(withText(R.string.uploads_media_title))
+ pressBack()
+ waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView))
+
+ assertDisplayed(R.id.roomProfileAvatarView)
+
+ // Leave
+ leaveRoom {
+ negativeAction()
+ }
+
+ // Advanced
+ // Room addresses
+
+ clickListItem(R.id.matrixProfileRecyclerView, 15)
+ waitUntilViewVisible(withText(R.string.room_alias_published_alias_title))
+ pressBack()
+
+ // Room permissions
+ clickListItem(R.id.matrixProfileRecyclerView, 17)
+ waitUntilViewVisible(withText(R.string.room_permissions_title))
+ clickOn(R.string.room_permissions_change_room_avatar)
+ waitUntilViewVisible(withId(android.R.id.button2))
+ clickDialogNegativeButton()
+ waitUntilViewVisible(withText(R.string.room_permissions_title))
+ // Toggle
+ clickOn(R.string.show_advanced)
+ clickOn(R.string.hide_advanced)
+ pressBack()
+
+ // Menu share
+ // clickMenu(R.id.roomProfileShareAction)
+ // pressBack()
+ }
+
+ private fun leaveRoom(block: DialogRobot.() -> Unit) {
+ clickListItem(R.id.matrixProfileRecyclerView, 13)
+ waitUntilViewVisible(withId(android.R.id.button2))
+ val dialogRobot = DialogRobot()
+ block(dialogRobot)
+ if (dialogRobot.returnedToPreviousScreen) {
+ waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView))
+ }
+ }
+
+ private fun navigateToRoomParameters() {
+ // Room history readability
+ clickListItem(R.id.roomSettingsRecyclerView, 4)
+ pressBack()
+
+ // Room access
+ clickListItem(R.id.roomSettingsRecyclerView, 6)
+ pressBack()
+ }
+
+ private fun navigateToInvite() {
+ assertDisplayed(R.id.inviteUsersButton)
+ clickOn(R.id.inviteUsersButton)
+ ViewActions.closeSoftKeyboard()
+ pressBack()
+ }
+
+ private fun navigateToRoomPeople() {
+ // Open first user
+ clickListItem(R.id.roomSettingsRecyclerView, 1)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.memberProfilePowerLevelView))
+ }
+
+ // Verification
+ clickListItem(R.id.matrixProfileRecyclerView, 1)
+ waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
+ pressBack()
+ waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView))
+
+ // Role
+ clickListItem(R.id.matrixProfileRecyclerView, 3)
+ waitUntilViewVisible(withId(android.R.id.button2))
+ clickDialogNegativeButton()
+ waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView))
+ pressBack()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt
new file mode 100644
index 0000000000..4aeb8903dd
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import im.vector.app.R
+import im.vector.app.espresso.tools.clickOnPreference
+import im.vector.app.espresso.tools.waitUntilViewVisible
+
+class SettingsAdvancedRobot {
+
+ fun crawl() {
+ clickOnPreference(R.string.settings_notifications_targets)
+ pressBack()
+
+ clickOnPreference(R.string.settings_push_rules)
+ pressBack()
+ }
+
+ fun toggleDeveloperMode() {
+ clickOn(R.string.settings_developer_mode_summary)
+ }
+
+ fun crawlDeveloperOptions() {
+ clickOnPreference(R.string.settings_account_data)
+ waitUntilViewVisible(withText("m.push_rules"))
+ clickOn("m.push_rules")
+ pressBack()
+ pressBack()
+ clickOnPreference(R.string.settings_key_requests)
+ pressBack()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsGeneralRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsGeneralRobot.kt
new file mode 100644
index 0000000000..9082eaa889
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsGeneralRobot.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import androidx.test.espresso.Espresso.pressBack
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
+import im.vector.app.R
+import im.vector.app.espresso.tools.clickOnPreference
+
+class SettingsGeneralRobot {
+
+ fun crawl() {
+ clickOn(R.string.settings_profile_picture)
+ clickDialogPositiveButton()
+ clickOn(R.string.settings_display_name)
+ clickDialogNegativeButton()
+ clickOn(R.string.settings_password)
+ clickDialogNegativeButton()
+ clickOn(R.string.settings_emails_and_phone_numbers_title)
+ pressBack()
+ clickOn(R.string.settings_discovery_manage)
+ clickOn(R.string.add_identity_server)
+ pressBack()
+ pressBack()
+ // Homeserver
+ clickOnPreference(R.string.settings_home_server)
+ pressBack()
+ // Identity server
+ clickOnPreference(R.string.settings_identity_server)
+ pressBack()
+ // Deactivate account
+ clickOnPreference(R.string.settings_deactivate_my_account)
+ pressBack()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt
new file mode 100644
index 0000000000..75f610d016
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
+import im.vector.app.R
+
+class SettingsHelpRobot {
+
+ fun crawl() {
+ /*
+ clickOn(R.string.settings_app_info_link_title)
+ Cannot go back...
+ pressBack()
+ clickOn(R.string.settings_copyright)
+ pressBack()
+ clickOn(R.string.settings_app_term_conditions)
+ pressBack()
+ clickOn(R.string.settings_privacy_policy)
+ pressBack()
+ */
+ clickOn(R.string.settings_third_party_notices)
+ clickDialogPositiveButton()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt
new file mode 100644
index 0000000000..448552ba8e
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import androidx.test.espresso.Espresso.pressBack
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import im.vector.app.BuildConfig
+import im.vector.app.R
+import im.vector.app.espresso.tools.clickOnPreference
+
+class SettingsNotificationsRobot {
+
+ fun crawl() {
+ if (BuildConfig.USE_NOTIFICATION_SETTINGS_V2) {
+ clickOn(R.string.settings_notification_default)
+ pressBack()
+ clickOn(R.string.settings_notification_mentions_and_keywords)
+ // TODO Test adding a keyword?
+ pressBack()
+ clickOn(R.string.settings_notification_other)
+ pressBack()
+ } else {
+ clickOn(R.string.settings_notification_advanced)
+ pressBack()
+ }
+ /*
+ clickOn(R.string.settings_noisy_notifications_preferences)
+ TODO Cannot go back
+ pressBack()
+ clickOn(R.string.settings_silent_notifications_preferences)
+ pressBack()
+ clickOn(R.string.settings_call_notifications_preferences)
+ pressBack()
+ */
+ clickOnPreference(R.string.settings_notification_troubleshoot)
+ pressBack()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt
new file mode 100644
index 0000000000..438bbac5a5
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsPreferencesRobot.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilViewVisible
+
+class SettingsPreferencesRobot {
+
+ fun crawl() {
+ clickOn(R.string.settings_interface_language)
+ waitUntilViewVisible(withText("Dansk (Danmark)"))
+ pressBack()
+ clickOn(R.string.settings_theme)
+ clickDialogNegativeButton()
+ clickOn(R.string.font_size)
+ clickDialogNegativeButton()
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
new file mode 100644
index 0000000000..a9c053f6c3
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.ui.robot.settings
+
+import im.vector.app.R
+import im.vector.app.clickOnAndGoBack
+
+class SettingsRobot {
+
+ fun toggleDeveloperMode() {
+ advancedSettings {
+ toggleDeveloperMode()
+ }
+ }
+
+ fun general(block: SettingsGeneralRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.settings_general_title) { block(SettingsGeneralRobot()) }
+ }
+
+ fun notifications(block: SettingsNotificationsRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.settings_notifications) { block(SettingsNotificationsRobot()) }
+ }
+
+ fun preferences(block: SettingsPreferencesRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.settings_preferences) { block(SettingsPreferencesRobot()) }
+ }
+
+ fun voiceAndVideo(block: () -> Unit = {}) {
+ clickOnAndGoBack(R.string.preference_voice_and_video) { block() }
+ }
+
+ fun ignoredUsers(block: () -> Unit = {}) {
+ clickOnAndGoBack(R.string.settings_ignored_users) { block() }
+ }
+
+ fun securityAndPrivacy(block: SettingsSecurityRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.settings_security_and_privacy) { block(SettingsSecurityRobot()) }
+ }
+
+ fun labs(block: () -> Unit = {}) {
+ clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
+ }
+
+ fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.settings_advanced_settings) {
+ block(SettingsAdvancedRobot())
+ }
+ }
+
+ fun helpAndAbout(block: SettingsHelpRobot.() -> Unit) {
+ clickOnAndGoBack(R.string.preference_root_help_about) { block(SettingsHelpRobot()) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/InstantRxRule.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt
similarity index 51%
rename from vector/src/test/java/im/vector/app/test/InstantRxRule.kt
rename to vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt
index 1145cb7dd1..f2607bbc1c 100644
--- a/vector/src/test/java/im/vector/app/test/InstantRxRule.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt
@@ -14,19 +14,24 @@
* limitations under the License.
*/
-package im.vector.app.test
+package im.vector.app.ui.robot.settings
-import io.reactivex.android.plugins.RxAndroidPlugins
-import io.reactivex.plugins.RxJavaPlugins
-import io.reactivex.schedulers.Schedulers
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
+import androidx.test.espresso.Espresso
+import im.vector.app.R
+import im.vector.app.espresso.tools.clickOnPreference
-class InstantRxRule : TestRule {
- override fun apply(base: Statement, description: Description?): Statement {
- RxJavaPlugins.setInitNewThreadSchedulerHandler { Schedulers.trampoline() }
- RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
- return base
+class SettingsSecurityRobot {
+
+ fun crawl() {
+ clickOnPreference(R.string.settings_active_sessions_show_all)
+ Espresso.pressBack()
+
+ clickOnPreference(R.string.encryption_message_recovery)
+ // TODO go deeper here
+ Espresso.pressBack()
+ /* Cannot exit
+ clickOnPreference(R.string.encryption_export_e2e_room_keys)
+ pressBack()
+ */
}
}
diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml
index 022b08f16d..ea9fa023ab 100644
--- a/vector/src/fdroid/AndroidManifest.xml
+++ b/vector/src/fdroid/AndroidManifest.xml
@@ -14,7 +14,9 @@
-
+
diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
index 0f375561b2..c1fda2d404 100644
--- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
+++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt
@@ -25,6 +25,7 @@ import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import im.vector.app.core.extensions.singletonEntryPoint
+import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.core.services.VectorSyncService
import org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber
@@ -67,7 +68,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
putExtra(SyncService.EXTRA_PERIODIC, true)
}
- val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val pIntent = PendingIntent.getBroadcast(
+ context,
+ REQUEST_CODE,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
+ )
val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L
val alarmMgr = context.getSystemService()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -80,7 +86,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
fun cancelAlarm(context: Context) {
Timber.v("## Sync: Cancel alarm for background sync")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
- val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val pIntent = PendingIntent.getBroadcast(
+ context,
+ REQUEST_CODE,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
+ )
val alarmMgr = context.getSystemService()!!
alarmMgr.cancel(pIntent)
diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml
index d849d5fb2d..f541eebd83 100755
--- a/vector/src/gplay/AndroidManifest.xml
+++ b/vector/src/gplay/AndroidManifest.xml
@@ -9,7 +9,9 @@
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
-
+
diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
index 63d50d4f97..e323506e9f 100755
--- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
+++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
@@ -201,8 +201,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
resolvedEvent
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
?.let {
- notificationDrawerManager.onNotifiableEventReceived(it)
- notificationDrawerManager.refreshNotificationDrawer()
+ notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) }
}
}
}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 376e0e869a..e5d4753235 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -4,7 +4,10 @@
package="im.vector.app">
-
+
+
@@ -339,6 +342,7 @@
+
@@ -417,6 +421,22 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/sdk_provider_paths" />
+
+
+
+
+
+
diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html
index 19daf3359b..529b7da2f1 100755
--- a/vector/src/main/assets/open_source_licenses.html
+++ b/vector/src/main/assets/open_source_licenses.html
@@ -279,25 +279,9 @@ SOFTWARE.
Copyright 2012 The Dagger Authors