Merge branch 'release/1.3.8' into main

This commit is contained in:
Benoit Marty 2021-11-17 16:04:44 +01:00
commit 879e6ef5e2
348 changed files with 8136 additions and 2918 deletions

View file

@ -23,7 +23,7 @@ body:
- type: textarea - type: textarea
id: result id: result
attributes: attributes:
label: Intended result and actual result label: Outcome
placeholder: Tell us what went wrong placeholder: Tell us what went wrong
value: | value: |
#### What did you expect? #### What did you expect?

View file

@ -10,7 +10,7 @@ body:
id: usecase id: usecase
attributes: attributes:
label: Your use case 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! placeholder: Tell us what you would like to do!
value: | value: |
#### What would you like to do? #### What would you like to do?

View file

@ -1,9 +1,9 @@
name: Sanity Test name: Sanity Test
on: on:
pull_request: { } schedule:
push: # At 20:00 every day UTC
branches: [ main, develop ] - cron: '0 20 * * *'
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
@ -14,13 +14,15 @@ env:
jobs: jobs:
integration-tests: integration-tests:
name: Sanity Tests (Synapse) name: Sanity Tests (Synapse)
runs-on: ubuntu-latest runs-on: macos-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
api-level: [28] api-level: [ 29 ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
ref: develop
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@ -46,11 +48,32 @@ jobs:
python3 -m venv .synapse python3 -m venv .synapse
source .synapse/bin/activate source .synapse/bin/activate
pip install synapse matrix-synapse pip install synapse matrix-synapse
curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \ curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
| sed s/127.0.0.1/0.0.0.0/g | bash | 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 }} - name: Run sanity tests on API ${{ matrix.api-level }}
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true # allow pipeline to upload failure results
with: with:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
api-level: ${{ matrix.api-level }} 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/

View file

@ -8,7 +8,7 @@ jobs:
automate-project-columns: automate-project-columns:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: alex-page/github-project-automation-plus@v0.8.1 - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with: with:
project: Issue triage project: Issue triage
column: Incoming column: Incoming

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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"

View file

@ -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 }}

View file

@ -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) Changes in Element v1.3.7 (2021-11-04)
====================================== ======================================

View file

@ -17,6 +17,7 @@
package im.vector.lib.attachmentviewer package im.vector.lib.attachmentviewer
import android.annotation.SuppressLint
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle 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 // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false) window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE // 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 // New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// new API instead of FLAG_TRANSLUCENT_NAVIGATION // 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 // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE // 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 // New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION // New API instead of FLAG_TRANSLUCENT_NAVIGATION

View file

@ -1,8 +1,8 @@
ext.versions = [ ext.versions = [
'minSdk' : 21, 'minSdk' : 21,
'compileSdk' : 30, 'compileSdk' : 31,
'targetSdk' : 30, 'targetSdk' : 31,
'sourceCompat' : JavaVersion.VERSION_11, 'sourceCompat' : JavaVersion.VERSION_11,
'targetCompat' : JavaVersion.VERSION_11, 'targetCompat' : JavaVersion.VERSION_11,
] ]
@ -11,13 +11,13 @@ def gradle = "7.0.3"
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
def kotlin = "1.5.31" def kotlin = "1.5.31"
def kotlinCoroutines = "1.5.2" def kotlinCoroutines = "1.5.2"
def dagger = "2.40" def dagger = "2.40.1"
def retrofit = "2.9.0" def retrofit = "2.9.0"
def arrow = "0.8.2" def arrow = "0.8.2"
def markwon = "4.6.2" def markwon = "4.6.2"
def moshi = "1.12.0" def moshi = "1.12.0"
def lifecycle = "2.2.0" def lifecycle = "2.4.0"
def rxBinding = "3.1.0" def flowBinding = "1.2.0"
def epoxy = "4.6.2" def epoxy = "4.6.2"
def mavericks = "2.4.0" def mavericks = "2.4.0"
def glide = "4.12.0" def glide = "4.12.0"
@ -26,7 +26,7 @@ def jjwt = "0.11.2"
def vanniktechEmoji = "0.8.0" def vanniktechEmoji = "0.8.0"
// Testing // Testing
def mockk = "1.12.0" def mockk = "1.12.1"
def espresso = "3.4.0" def espresso = "3.4.0"
def androidxTest = "1.4.0" def androidxTest = "1.4.0"
@ -41,22 +41,23 @@ ext.libs = [
jetbrains : [ jetbrains : [
'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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 : [ androidx : [
'appCompat' : "androidx.appcompat:appcompat:1.3.1", '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", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1", '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", 'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
'junit' : "androidx.test.ext:junit:1.1.3", 'junit' : "androidx.test.ext:junit:1.1.3",
'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle", 'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle", 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1", 'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
'datastore' : "androidx.datastore:datastore:1.0.0", 'datastore' : "androidx.datastore:datastore:1.0.0",
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
@ -102,7 +103,6 @@ ext.libs = [
'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy", 'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy",
'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy", 'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy",
'mavericks' : "com.airbnb.android:mavericks:$mavericks", 'mavericks' : "com.airbnb.android:mavericks:$mavericks",
'mavericksRx' : "com.airbnb.android:mavericks-rxjava2:$mavericks",
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
], ],
mockk : [ mockk : [
@ -115,13 +115,13 @@ ext.libs = [
'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer", 'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer",
'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer", 'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer",
'progressPieIndicator' : "com.github.piasy:ProgressPieIndicator:$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 : [ jakewharton : [
'timber' : "com.jakewharton.timber:timber:5.0.1", '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"
], ],
jsonwebtoken: [ jsonwebtoken: [
'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt", 'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt",

41
docs/rx_flow_migration.md Normal file
View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -1,2 +1,2 @@
Main changes in this version: Bug fixes mainly regarding the notifications. 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 Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1 +1 @@
Groepsberjochtetsjinst - fersifere berjochten, groeps petearen en fideo skilje Groepsberjochtetsjinst - fersifere berjochten, groepspetearen en fideobelje

View file

@ -1 +1 @@
Element - Feilige Berjochtetsjinst Element - Feilige berjochtetsjinst

View file

@ -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

View file

@ -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

View file

@ -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 Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.4

View file

@ -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

View file

@ -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

View file

@ -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.
<b>Fitur Element termasuk:</b> <b>Fitur Element termasuk</b>
- Alat komunikasi online yang canggih - Alat komunikasi online yang canggih
- Pesan terenkripsi sepenuhnya untuk memungkinkan komunikasi perusahaan yang lebih aman, bahkan untuk pekerja jarak jauh - Pesan-pesan yang dienkripsi sepenuhnya untuk memungkinkan komunikasi perusahaan yang lebih aman, bahkan untuk pekerja jarak jauh
- Obrolan terdesentralisasi berdasarkan framework sumber-terbuka Matrix - Obrolan terdesentralisasi berdasarkan kerangka Matrix yang sumber terbuka
- Berbagi file dengan aman dengan data terenkripsi saat mengelola proyek - Pembagian file aman dengan data terenkripsi saat mengelola proyek
- Obrolan video dengan VoIP dan berbagi layar - 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 - 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.
<b>Pesan privasi dan terenkripsi</b> <b>Perpesanan dengan privasi dan enkripsi</b>
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. 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.
<b>Element dapat dihost sendiri</b> <b>Element dapat dihost sendiri</b>
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.
<b>Miliki data Anda</b> <b>Miliki data Anda</b>
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: 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 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 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
<b>Pesan terbuka dan kolaborasi</b> <b>Pesan terbuka dan kolaborasi</b>
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.
<b>Sangat aman</b> <b>Sangat aman</b>
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.
<b>Komunikasi dan integrasi lengkap</b> <b>Komunikasi dan integrasi lengkap</b>
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.
<b>Ambil di mana Anda tinggalkan</b> <b>Ambil di mana Anda tinggalkan</b>
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
<b>Open source</b> <b>Sumber terbuka</b>
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 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

View file

@ -1 +1 @@
Perpesanan grup - pesan terenkripsi, panggilan grup dan video Perpesanan grup - perpesanan, panggilan suara dan video grup terenkripsi

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.4

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,2 +1,2 @@
Основні зміни в цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнено на matrix.org. Знову додано підтримку Android Auto. Основні зміни в цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнено на matrix.org). Знову додано підтримку Android Auto.
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.4 Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.4

View file

@ -0,0 +1,2 @@
Основні зміни у цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнена на matrix.org). Знову додано підтримку Android Auto.
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.5

View file

@ -0,0 +1,2 @@
Основні зміни у цій версії: Додано підтримку присутності для кімнати особистих повідомлень (примітка: присутність вимкнена на matrix.org). Знову додано підтримку Android Auto.
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.6

View file

@ -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 完整更新日志https://github.com/vector-im/element-android/releases/tag/v1.3.4

View file

@ -0,0 +1,2 @@
此版本的主要变化:为私信聊天室添加 Presence 支持 (注意:在 matrix.org 上 Presence 是禁用的)。再次添加 Android Auto 支持。
完整更新日志https://github.com/vector-im/element-android/releases/tag/v1.3.5

View file

@ -0,0 +1,2 @@
此版本的主要变化:为私信聊天室添加 Presence 支持(注意:在 matrix.org 上 Presence 是禁用的)。再次添加 Android Auto 支持。
完整更新日志https://github.com/vector-im/element-android/releases/tag/v1.3.6

View file

@ -0,0 +1,2 @@
此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。
完整的變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.3.5

View file

@ -0,0 +1,2 @@
此版本中的主要變動:為直接訊息聊天室新增 Presence 支援(請注意:此功能在 matrix.org 上停用)。加回 Android Auto 支援。
完整的變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.3.6

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b distributionSha256Sum=00b273629df4ce46e68df232161d5a7c4e495b9a029ce6e0420f071e21316867
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?vctr_content_tertiary" android:state_enabled="false" />
<item android:color="?vctr_content_tertiary" android:state_focused="false" />
<item android:color="?colorPrimary" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorPrimary" android:state_focused="true"/>
<item android:color="?colorPrimary" android:state_hovered="true"/>
<item android:color="?colorPrimary" android:state_enabled="false"/>
<item android:color="?vctr_content_quinary"/>
</selector>

View file

@ -129,9 +129,9 @@
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color> <color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color> <color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
<attr name="vctr_voice_message_toast_background" format="color" /> <attr name="vctr_toast_background" format="color" />
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color> <color name="vctr_toast_background_light">@color/palette_black_900</color>
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color> <color name="vctr_toast_background_dark">@color/palette_gray_400</color>
<!-- Presence Indicator colors --> <!-- Presence Indicator colors -->
<attr name="vctr_presence_indicator_offline" format="color" /> <attr name="vctr_presence_indicator_offline" format="color" />

View file

@ -10,6 +10,11 @@
<item name="lineHeight">24sp</item> <item name="lineHeight">24sp</item>
</style> </style>
<style name="Widget.Vector.Button.CallToAction" parent="Widget.Vector.Button">
<item name="android:backgroundTint">@color/button_background_tint_selector</item>
<item name="android:textColor">@android:color/white</item>
</style>
<style name="Widget.Vector.Button.Destructive"> <style name="Widget.Vector.Button.Destructive">
<item name="android:minWidth">94dp</item> <item name="android:minWidth">94dp</item>
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item> <item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>

View file

@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Default style for TextInputLayout -->
<style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
<style name="Widget.Vector.TextInputLayout.Password">
<item name="endIconMode">password_toggle</item>
<item name="endIconTint">?vctr_content_secondary</item>
</style>
<style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText"> <style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText">
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
<item name="android:inputType">textCapSentences|textMultiLine</item> <item name="android:inputType">textCapSentences|textMultiLine</item>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Default style for TextInputLayout -->
<style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
<style name="Widget.Vector.TextInputLayout.Password">
<item name="endIconMode">password_toggle</item>
<item name="endIconTint">?vctr_content_secondary</item>
</style>
<style name="Widget.Vector.TextInputLayout.Form">
<item name="boxStrokeColor">@color/form_edit_text_stroke_color_selector</item>
<item name="android:textColorHint">@color/form_edit_text_hint_color_selector</item>
</style>
</resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.Vector.TextView.Caption.Toast">
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:background">@drawable/bg_round_corner_8dp</item>
<item name="android:backgroundTint">?vctr_toast_background</item>
<item name="android:textColor">@color/palette_white</item>
<item name="android:gravity">center</item>
</style>
</resources>

View file

@ -12,15 +12,4 @@
<item name="direction">rightToLeft</item> <item name="direction">rightToLeft</item>
</style> </style>
<style name="Widget.Vector.TextView.Caption.Toast">
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:background">@drawable/bg_round_corner_8dp</item>
<item name="android:backgroundTint">?vctr_voice_message_toast_background</item>
<item name="android:textColor">@color/palette_white</item>
<item name="android:gravity">center</item>
</style>
</resources> </resources>

View file

@ -140,8 +140,7 @@
<!-- Keywords --> <!-- Keywords -->
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item> <item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<!-- Voice Message --> <item name="vctr_toast_background">@color/vctr_toast_background_dark</item>
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
</style> </style>
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" /> <style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

View file

@ -143,8 +143,7 @@
<!-- Keywords --> <!-- Keywords -->
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item> <item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<!-- Voice Message --> <item name="vctr_toast_background">@color/vctr_toast_background_light</item>
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
</style> </style>
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" /> <style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />

View file

@ -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.group.model.GroupSummary
import org.matrix.android.sdk.api.session.identity.ThreePid 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.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.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState 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) { class FlowSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Flow<List<RoomSummary>> { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow<List<RoomSummary>> {
return session.getRoomSummariesLive(queryParams).asFlow() return session.getRoomSummariesLive(queryParams, sortOrder).asFlow()
.startWith(session.coroutineDispatchers.io) { .startWith(session.coroutineDispatchers.io) {
session.getRoomSummaries(queryParams) session.getRoomSummaries(queryParams, sortOrder)
} }
} }

View file

@ -9,7 +9,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.3.7\"" buildConfigField "String", "SDK_VERSION", "\"1.3.8\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
@ -44,6 +44,7 @@ android {
} }
testOptions { testOptions {
// Comment to run on Android 12
execution 'ANDROIDX_TEST_ORCHESTRATOR' execution 'ANDROIDX_TEST_ORCHESTRATOR'
} }
@ -106,8 +107,9 @@ dependencies {
implementation libs.androidx.appCompat implementation libs.androidx.appCompat
implementation libs.androidx.core implementation libs.androidx.core
implementation libs.androidx.lifecycleExtensions // Lifecycle
implementation libs.androidx.lifecycleJava8 implementation libs.androidx.lifecycleCommon
implementation libs.androidx.lifecycleProcess
// Network // Network
implementation libs.squareup.retrofit implementation libs.squareup.retrofit
@ -156,10 +158,10 @@ dependencies {
implementation libs.apache.commonsImaging implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber // 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 libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.6.1' testImplementation 'org.robolectric:robolectric:4.7'
//testImplementation 'org.robolectric:shadows-support-v4:3.0' //testImplementation 'org.robolectric:shadows-support-v4:3.0'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation libs.mockk.mockk testImplementation libs.mockk.mockk

View file

@ -20,7 +20,18 @@ package org.matrix.android.sdk.api
* This class contains pattern to match Matrix Url, aka mxc urls * This class contains pattern to match Matrix Url, aka mxc urls
*/ */
object MatrixUrls { object MatrixUrls {
/**
* "mxc" scheme, including "://". So "mxc://"
*/
const val MATRIX_CONTENT_URI_SCHEME = "mxc://" const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
/**
* Return true if the String starts with "mxc://"
*/
fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME) 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)
} }

View file

@ -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.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull 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 org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException import java.io.IOException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
error.code == MatrixError.M_INVALID_USERNAME || error.code == MatrixError.M_INVALID_USERNAME ||
error.code == MatrixError.M_EXCLUSIVE) 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
}
}

View file

@ -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<Pair<Event, PushRule>>,
val roomsJoined: Collection<String>,
val roomsLeft: Collection<String>,
val redactedEventIds: List<String>
)

View file

@ -51,11 +51,7 @@ interface PushRuleService {
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? // fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
interface PushRuleListener { interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>) fun onEvents(pushEvents: PushEvents)
fun onRoomJoined(roomId: String)
fun onRoomLeft(roomId: String)
fun onEventRedacted(redactedEventId: String)
fun batchFinish()
} }
fun getKeywords(): LiveData<Set<String>> fun getKeywords(): LiveData<Set<String>>

View file

@ -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.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker 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.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.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@ -192,6 +193,11 @@ interface Session :
*/ */
fun cryptoService(): CryptoService fun cryptoService(): CryptoService
/**
* Returns the ContentScannerService associated with the session
*/
fun contentScannerService(): ContentScannerService
/** /**
* Returns the identity service associated with the session * Returns the identity service associated with the session
*/ */

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.content 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. * This interface defines methods for accessing content from the current session.
*/ */
@ -39,6 +41,15 @@ interface ContentUrlResolver {
*/ */
fun resolveFullSize(contentUrl: String?): String? 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. * 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. * @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? 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()
}
} }

View file

@ -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

View file

@ -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<Optional<ScanStatusInfo>>
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
}

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,18 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.utils package org.matrix.android.sdk.api.session.contentscanner
import io.reactivex.Completable enum class ScanState {
import io.reactivex.Single TRUSTED,
import io.reactivex.disposables.Disposable INFECTED,
import io.reactivex.internal.functions.Functions UNKNOWN,
import timber.log.Timber IN_PROGRESS
fun <T> Single<T>.subscribeLogError(): Disposable {
return subscribe(Functions.emptyConsumer(), { Timber.e(it) })
} }
fun Completable.subscribeLogError(): Disposable { data class ScanStatusInfo(
return subscribe({}, { Timber.e(it) }) val state: ScanState,
} val scanDateTimestamp: Long?,
val humanReadableMessage: String?
)

View file

@ -102,6 +102,9 @@ object EventType {
// Relation Events // Relation Events
const val REACTION = "m.reaction" const val REACTION = "m.reaction"
// Poll
const val POLL_START = "org.matrix.msc3381.poll.start"
// Unwedging // Unwedging
internal const val DUMMY = "m.dummy" internal const val DUMMY = "m.dummy"

View file

@ -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
)

View file

@ -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
)

View file

@ -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<PollAnswer>? = null
)

View file

@ -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
)

View file

@ -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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.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.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -84,10 +83,10 @@ interface SendService {
/** /**
* Send a poll to the room. * Send a poll to the room.
* @param question the question * @param question the question
* @param options list of (label, value) * @param options list of options
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendPoll(question: String, options: List<OptionItem>): Cancelable fun sendPoll(question: String, options: List<String>): Cancelable
/** /**
* Method to send a poll response. * Method to send a poll response.

View file

@ -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.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.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.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.sender.SenderInfo 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 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<MessageContent>()?.body
?: lastMessageContent.body
}
return null
}
/** /**
* Returns true if it's a reply * Returns true if it's a reply
*/ */
@ -156,11 +144,25 @@ fun TimelineEvent.isEdition(): Boolean {
return root.isEdition() 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 if (isReply()) {
return extractUsefulTextFromReply(lastContent?.body ?: "") extractUsefulTextFromReply(lastContentBody)
} else { } else {
lastContent?.body ?: "" lastContentBody
} }
} }
/**
* Get the latest displayable content.
* Will take care to hide spoiler text
*/
fun MessageContent.getTextDisplayableContent(): String {
return newContent?.toModel<MessageTextContent>()?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
?: newContent?.toModel<MessageContent>()?.body
?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
?: body
}

View file

@ -15,6 +15,8 @@
*/ */
package org.matrix.android.sdk.api.util package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.internal.util.unescapeHtml
object ContentUtils { object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String { fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines() val lines = repliedBody.lines()
@ -44,4 +46,15 @@ object ContentUtils {
} }
return repliedBody 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("(?<=<span data-mx-spoiler)=\\\".+?\\\">".toRegex(), ">")
.replace("(?<=<span data-mx-spoiler>).+?(?=</span>)".toRegex()) { SPOILER_CHAR.repeat(it.value.length) }
.unescapeHtml()
}
private const val SPOILER_CHAR = ""
} }

View file

@ -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 { fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
DaggerSessionComponent DaggerSessionComponent

View file

@ -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.auth.registration.RegisterAddThreePidTask
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
internal class DefaultLoginWizard( internal class DefaultLoginWizard(
private val authAPI: AuthAPI, private val authAPI: AuthAPI,
@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
private val getProfileTask: GetProfileTask = DefaultGetProfileTask( private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI, authAPI,
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig) DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
) )
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo { override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {

View file

@ -38,14 +38,22 @@ data class MXKey(
/** /**
* signature user Id to [deviceid][signature] * signature user Id to [deviceid][signature]
*/ */
private val signatures: Map<String, Map<String, String>> private val signatures: Map<String, Map<String, String>>,
/**
* 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 * @return the signed data map
*/ */
fun signalableJSONDictionary(): Map<String, Any> { fun signalableJSONDictionary(): Map<String, Any> {
return mapOf("key" to value) return rawMap.filter {
it.key != "signatures" && it.key != "unsigned"
}
} }
/** /**
@ -82,6 +90,7 @@ data class MXKey(
* <pre> * <pre>
* "signed_curve25519:AAAAFw": { * "signed_curve25519:AAAAFw": {
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4", * "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
* "fallback" : true|false
* "signatures": { * "signatures": {
* "@userId:matrix.org": { * "@userId:matrix.org": {
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg" * "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
@ -107,7 +116,8 @@ data class MXKey(
type = components[0], type = components[0],
keyId = components[1], keyId = components[1],
value = params["key"] as String, value = params["key"] as String,
signatures = params["signatures"] as Map<String, Map<String, String>> signatures = params["signatures"] as Map<String, Map<String, String>>,
rawMap = params
) )
} }
} }

View file

@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class IdentityDatabase internal annotation class IdentityDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ContentScannerDatabase

View file

@ -38,6 +38,9 @@ internal object NetworkConstants {
// Integration // Integration
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/" 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 // Federation
const val URI_FEDERATION_PATH = "_matrix/federation/v1/" const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
} }

View file

@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.completeWith import kotlinx.coroutines.completeWith
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.content.ContentUrlResolver 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) val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
if (!cachedFiles.file.exists()) { 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() val request = when (resolvedUrl) {
.url(resolvedUrl) is ContentUrlResolver.ResolvedMethod.GET -> {
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) Request.Builder()
.build() .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 { val response = try {
okHttpClient.newCall(request).execute() okHttpClient.newCall(request).execute()

View file

@ -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.call.CallSignalingService
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker 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.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.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>, private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>, private val accountService: Lazy<AccountService>,
private val eventService: Lazy<EventService>, private val eventService: Lazy<EventService>,
private val contentScannerService: Lazy<ContentScannerService>,
private val identityService: IdentityService, private val identityService: IdentityService,
private val integrationManagerService: IntegrationManagerService, private val integrationManagerService: IntegrationManagerService,
private val thirdPartyService: Lazy<ThirdPartyService>, private val thirdPartyService: Lazy<ThirdPartyService>,
@ -174,8 +176,8 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach { lifecycleObservers.forEach {
it.onSessionStarted(this) it.onSessionStarted(this)
} }
sessionListeners.dispatch { _, listener -> dispatchTo(sessionListeners) { session, listener ->
listener.onSessionStarted(this) listener.onSessionStarted(session)
} }
} }
} }
@ -217,8 +219,8 @@ internal class DefaultSession @Inject constructor(
// timelineEventDecryptor.destroy() // timelineEventDecryptor.destroy()
uiHandler.post { uiHandler.post {
lifecycleObservers.forEach { it.onSessionStopped(this) } lifecycleObservers.forEach { it.onSessionStopped(this) }
sessionListeners.dispatch { _, listener -> dispatchTo(sessionListeners) { session, listener ->
listener.onSessionStopped(this) listener.onSessionStopped(session)
} }
} }
cryptoService.get().close() cryptoService.get().close()
@ -249,8 +251,8 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach { lifecycleObservers.forEach {
it.onClearCache(this) it.onClearCache(this)
} }
sessionListeners.dispatch { _, listener -> dispatchTo(sessionListeners) { session, listener ->
listener.onClearCache(this) listener.onClearCache(session)
} }
} }
withContext(NonCancellable) { withContext(NonCancellable) {
@ -260,8 +262,8 @@ internal class DefaultSession @Inject constructor(
} }
override fun onGlobalError(globalError: GlobalError) { override fun onGlobalError(globalError: GlobalError) {
sessionListeners.dispatch { _, listener -> dispatchTo(sessionListeners) { session, listener ->
listener.onGlobalError(this, globalError) listener.onGlobalError(session, globalError)
} }
} }
@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
override fun cryptoService(): CryptoService = cryptoService.get() override fun cryptoService(): CryptoService = cryptoService.get()
override fun contentScannerService(): ContentScannerService = contentScannerService.get()
override fun identityService() = identityService override fun identityService() = identityService
override fun fileService(): FileService = defaultFileService.get() override fun fileService(): FileService = defaultFileService.get()

View file

@ -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.call.CallModule
import org.matrix.android.sdk.internal.session.content.ContentModule 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.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.filter.FilterModule
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.group.GroupModule import org.matrix.android.sdk.internal.session.group.GroupModule
@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
AccountModule::class, AccountModule::class,
FederationModule::class, FederationModule::class,
CallModule::class, CallModule::class,
ContentScannerModule::class,
SearchModule::class, SearchModule::class,
ThirdPartyModule::class, ThirdPartyModule::class,
SpaceModule::class, SpaceModule::class,

View file

@ -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.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class SessionListeners @Inject constructor( internal class SessionListeners @Inject constructor() {
@SessionId private val sessionId: String,
private val sessionManager: SessionManager) {
private val listeners = mutableSetOf<Session.Listener>() private val listeners = mutableSetOf<Session.Listener>()
@ -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) { synchronized(listeners) {
val session = getSession() ?: return Unit.also {
Timber.w("You don't have any attached session")
}
listeners.forEach { listeners.forEach {
tryOrNull { block(session, it) } 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)
} }

View file

@ -44,7 +44,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
override suspend fun execute(params: DeactivateAccountTask.Params) { override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData) val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
cleanupSession.stopActiveTasks()
val canCleanup = try { val canCleanup = try {
executeRequest(globalErrorReceiver) { executeRequest(globalErrorReceiver) {
accountAPI.deactivate(deactivateAccountParams) accountAPI.deactivate(deactivateAccountParams)
@ -71,7 +71,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
runCatching { identityDisconnectTask.execute(Unit) } runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") } .onFailure { Timber.w(it, "Unable to disconnect identity server") }
cleanupSession.handle() cleanupSession.cleanup()
} }
} }
} }

View file

@ -50,20 +50,26 @@ internal class CleanupSession @Inject constructor(
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String @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 sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration) val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)") 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...") Timber.d("Cleanup: release session...")
sessionManager.releaseSession(sessionId) sessionManager.releaseSession(sessionId)
Timber.d("Cleanup: delete session params...")
sessionParamsStore.delete(sessionId)
Timber.d("Cleanup: clear session data...") Timber.d("Cleanup: clear session data...")
clearSessionDataTask.execute(Unit) clearSessionDataTask.execute(Unit)

View file

@ -16,20 +16,45 @@
package org.matrix.android.sdk.internal.session.content 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.isMxcUrl
import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig 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.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.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 org.matrix.android.sdk.internal.util.ensureTrailingSlash
import javax.inject.Inject 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() private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" 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? { override fun resolveFullSize(contentUrl: String?): String? {
return contentUrl return contentUrl
// do not allow non-mxc content URLs // do not allow non-mxc content URLs
@ -37,7 +62,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let { ?.let {
resolve( resolve(
contentUrl = it, contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/" toThumbnail = false
) )
} }
} }
@ -49,16 +74,27 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
?.let { ?.let {
resolve( resolve(
contentUrl = it, contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/", toThumbnail = true,
params = "?width=$width&height=$height&method=${method.value}" params = "?width=$width&height=$height&method=${method.value}"
) )
} }
} }
private fun resolve(contentUrl: String, private fun resolve(contentUrl: String,
prefix: String, toThumbnail: Boolean,
params: String = ""): String? { params: String = ""): String {
var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME) 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("#") val fragmentOffset = serverAndMediaId.indexOf("#")
var fragment = "" var fragment = ""
if (fragmentOffset >= 0) { if (fragmentOffset >= 0) {
@ -66,6 +102,11 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) 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
} }
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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<OkHttpClient>,
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<Optional<ScanStatusInfo>> {
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
}
}

View file

@ -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<Optional<ScanStatusInfo>> {
return MutableLiveData()
}
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
return null
}
}

View file

@ -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)
}
}
}

View file

@ -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<Optional<ScanStatusInfo>>
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)
}

View file

@ -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
}

View file

@ -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<ContentScanResultEntity>()
.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<ContentScanResultEntity>().also {
it.mediaUrl = attachmentUrl
it.scanDateTimestamp = System.currentTimeMillis()
it.scannerUrl = contentScannerUrl
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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<ContentScannerInfoEntity>()
}, {
it.serverUrl
}
).firstOrNull()
}
override fun setScannerUrl(url: String?) {
monarchy.runTransactionSync { realm ->
val info = realm.where<ContentScannerInfoEntity>().findFirst()
?: realm.createObject()
info.serverUrl = url
}
}
override fun enableScanner(enabled: Boolean) {
monarchy.runTransactionSync { realm ->
val info = realm.where<ContentScannerInfoEntity>().findFirst()
?: realm.createObject()
info.enabled = enabled
}
}
override fun isScanEnabled(): Boolean {
return monarchy.fetchAllMappedSync(
{ realm ->
realm.where<ContentScannerInfoEntity>()
}, {
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<ContentScanResultEntity>()
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
.apply {
getScannerUrl()?.let {
equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
}
}
}, {
it.toModel()
})
.firstOrNull()
}
override fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<ContentScanResultEntity>()
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
.equalTo(ContentScanResultEntityFields.SCANNER_URL, getScannerUrl())
},
{ entity ->
entity.toModel()
}
)
return Transformations.map(liveData) {
it.firstOrNull().toOptional()
}
}
}

Some files were not shown because too many files have changed in this diff Show more