mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 02:45:53 +03:00
Merge tag 'v1.4.8' into merge-v1.4.8
Change-Id: Ic6338fef4f27ca95016eb11ab07a10d3f3a8986e Conflicts: library/ui-styles/src/main/res/values/dimens.xml library/ui-styles/src/main/res/values/styles_buttons.xml matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt vector/src/main/res/drawable/ic_home_bottom_catchup.xml vector/src/main/res/drawable/ic_shield_custom.xml vector/src/main/res/drawable/ic_shield_trusted.xml vector/src/main/res/drawable/ic_shield_trusted_no_border.xml vector/src/main/res/layout/fragment_ftue_account_created.xml vector/src/main/res/layout/fragment_timeline.xml vector/src/main/res/layout/item_timeline_event_code_block_stub.xml vector/src/main/res/layout/view_attachment_type_selector.xml vector/src/main/res/layout/view_room_detail_toolbar.xml vector/src/main/res/menu/menu_submit.xml
This commit is contained in:
commit
d5d555d07f
435 changed files with 10318 additions and 3867 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
@ -49,7 +49,7 @@ jobs:
|
|||
if: github.ref == 'refs/heads/main'
|
||||
# Only runs on main, no concurrency.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
|
|
@ -7,5 +7,5 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
|
79
.github/workflows/nightly.yml
vendored
79
.github/workflows/nightly.yml
vendored
|
@ -14,50 +14,6 @@ env:
|
|||
-Porg.gradle.jvmargs=-Xmx4g
|
||||
-Porg.gradle.parallel=false
|
||||
jobs:
|
||||
# Build Android Tests [Matrix SDK]
|
||||
build-android-test-matrix-sdk:
|
||||
name: Matrix SDK - Build Android Tests
|
||||
runs-on: macos-latest
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build Android Tests for matrix-sdk-android
|
||||
run: ./gradlew clean matrix-sdk-android:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
||||
|
||||
# Build Android Tests [Matrix APP]
|
||||
build-android-test-app:
|
||||
name: App - Build Android Tests
|
||||
runs-on: macos-latest
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build Android Tests for vector
|
||||
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
||||
|
||||
# Run Android Tests
|
||||
integration-tests:
|
||||
name: Matrix SDK - Running Integration Tests
|
||||
|
@ -68,7 +24,7 @@ jobs:
|
|||
api-level: [ 28 ]
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
|
@ -87,11 +43,11 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Start synapse server
|
||||
run: |
|
||||
pip install matrix-synapse
|
||||
curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
|
||||
chmod 777 start.sh
|
||||
./start.sh --no-rate-limit
|
||||
uses: michaelkaye/setup-matrix-synapse@v0.3.0
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8080
|
||||
disableRateLimiting: true
|
||||
# package: org.matrix.android.sdk.session
|
||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
|
@ -260,7 +216,7 @@ jobs:
|
|||
api-level: [ 28 ]
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
|
@ -274,10 +230,11 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Start synapse server
|
||||
run: |
|
||||
pip install matrix-synapse
|
||||
curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
|
||||
| sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
|
||||
uses: michaelkaye/setup-matrix-synapse@v0.3.0
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8080
|
||||
disableRateLimiting: true
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
|
@ -308,9 +265,10 @@ jobs:
|
|||
failure_screenshots/
|
||||
|
||||
codecov-units:
|
||||
name: Unit tests with code coverage
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
|
@ -333,12 +291,13 @@ jobs:
|
|||
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
||||
|
||||
sonarqube:
|
||||
name: Sonarqube upload
|
||||
runs-on: macos-latest
|
||||
if: always()
|
||||
needs:
|
||||
- codecov-units
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
|
@ -362,13 +321,11 @@ jobs:
|
|||
|
||||
# Notify the channel about scheduled runs, do not notify for manually triggered runs
|
||||
notify:
|
||||
name: Notify matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- integration-tests
|
||||
- ui-tests
|
||||
# - unit-tests
|
||||
- build-android-test-matrix-sdk
|
||||
- build-android-test-app
|
||||
- sonarqube
|
||||
if: always() && github.event_name != 'workflow_dispatch'
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
|
@ -379,4 +336,4 @@ jobs:
|
|||
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
||||
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
||||
text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
||||
|
|
8
.github/workflows/quality.yml
vendored
8
.github/workflows/quality.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
name: Project Check Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run code quality check suite
|
||||
run: ./tools/check/check_code_quality.sh
|
||||
|
||||
|
@ -23,7 +23,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run ktlint
|
||||
run: |
|
||||
./gradlew ktlintCheck --continue
|
||||
|
@ -96,7 +96,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
@ -129,7 +129,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
if: github.repository == 'vector-im/element-android'
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
if: github.repository == 'vector-im/element-android'
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
|
@ -64,7 +64,7 @@ jobs:
|
|||
if: github.repository == 'vector-im/element-android'
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run analytics import script
|
||||
run: ./tools/import_analytic_plan.sh
|
||||
- name: Create Pull Request for analytics plan
|
||||
|
|
43
.github/workflows/tests.yml
vendored
43
.github/workflows/tests.yml
vendored
|
@ -12,6 +12,30 @@ env:
|
|||
-Porg.gradle.parallel=false
|
||||
|
||||
jobs:
|
||||
# Build Android Tests
|
||||
build-android-tests:
|
||||
name: Build Android Tests
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build Android Tests
|
||||
run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
||||
|
||||
unit-tests:
|
||||
name: Run Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -20,7 +44,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
|
@ -41,3 +65,20 @@ jobs:
|
|||
( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
|
||||
with:
|
||||
files: ./**/build/test-results/**/*.xml
|
||||
|
||||
# Notify the channel about runs against develop or main that have failures, as PRs should have caught these first.
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- unit-tests
|
||||
- build-android-tests
|
||||
if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) && failure() }}
|
||||
steps:
|
||||
- uses: michaelkaye/matrix-hookshot-action@v0.3.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
||||
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
||||
text_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}{{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion }} {{name}} <font color='{{color conclusion }}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
||||
|
||||
|
|
2
.github/workflows/update-gradle-wrapper.yml
vendored
2
.github/workflows/update-gradle-wrapper.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<w>emoji</w>
|
||||
<w>emojis</w>
|
||||
<w>fdroid</w>
|
||||
<w>ganfra</w>
|
||||
<w>gplay</w>
|
||||
<w>hmac</w>
|
||||
<w>homeserver</w>
|
||||
|
@ -18,6 +19,7 @@
|
|||
<w>ktlint</w>
|
||||
<w>linkified</w>
|
||||
<w>linkify</w>
|
||||
<w>manu</w>
|
||||
<w>megolm</w>
|
||||
<w>msisdn</w>
|
||||
<w>msisdns</w>
|
||||
|
|
71
CHANGES.md
71
CHANGES.md
|
@ -1,3 +1,74 @@
|
|||
Changes in Element v1.4.8 (2022-03-28)
|
||||
======================================
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Moving live location sharing permission to debug only builds whilst it is WIP ([#5636](https://github.com/vector-im/element-android/issues/5636))
|
||||
|
||||
|
||||
Changes in Element v1.4.7 (2022-03-24)
|
||||
======================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Fix inconsistencies between the arrow visibility and the collapse action on the room sections ([#5616](https://github.com/vector-im/element-android/issues/5616))
|
||||
- Fix room list header count flickering
|
||||
|
||||
Changes in Element v1.4.6 (2022-03-23)
|
||||
======================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Thread timeline is now live and much faster especially for large or old threads ([#5230](https://github.com/vector-im/element-android/issues/5230))
|
||||
- View all threads per room screen is now live when the home server supports threads ([#5232](https://github.com/vector-im/element-android/issues/5232))
|
||||
- Add a custom view to display a picker for share location options ([#5395](https://github.com/vector-im/element-android/issues/5395))
|
||||
- Add ability to pin a location on map for sharing ([#5417](https://github.com/vector-im/element-android/issues/5417))
|
||||
- Poll Integration Tests ([#5522](https://github.com/vector-im/element-android/issues/5522))
|
||||
- Live location sharing: adding build config field and show permission dialog ([#5536](https://github.com/vector-im/element-android/issues/5536))
|
||||
- Live location sharing: Adding indicator view when enabled ([#5571](https://github.com/vector-im/element-android/issues/5571))
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Poll system notifications on Android are not user friendly ([#4780](https://github.com/vector-im/element-android/issues/4780))
|
||||
- Add colors for shield vector drawable ([#4860](https://github.com/vector-im/element-android/issues/4860))
|
||||
- Support both stable and unstable prefixes for Events about Polls and Location ([#5340](https://github.com/vector-im/element-android/issues/5340))
|
||||
- Fix missing messages when loading messages forwards ([#5448](https://github.com/vector-im/element-android/issues/5448))
|
||||
- Fix presence indicator being aligned to the center of the room image ([#5489](https://github.com/vector-im/element-android/issues/5489))
|
||||
- Read receipt in wrong order ([#5514](https://github.com/vector-im/element-android/issues/5514))
|
||||
- Fix mentions using matrix.to rather than client defined permalink base url ([#5521](https://github.com/vector-im/element-android/issues/5521))
|
||||
- Fixes crash when tapping the timeline verification surround box instead of the buttons ([#5540](https://github.com/vector-im/element-android/issues/5540))
|
||||
- [Notification mode] Wrong mode is displayed when the mention only is selected on the web client ([#5547](https://github.com/vector-im/element-android/issues/5547))
|
||||
- Fix local echos not being shown when re-opening rooms ([#5551](https://github.com/vector-im/element-android/issues/5551))
|
||||
- Fix crash when closing a room while decrypting timeline events ([#5552](https://github.com/vector-im/element-android/issues/5552))
|
||||
- Fix sometimes read marker not properly updating ([#5564](https://github.com/vector-im/element-android/issues/5564))
|
||||
|
||||
In development 🚧
|
||||
----------------
|
||||
- Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities ([#5375](https://github.com/vector-im/element-android/issues/5375))
|
||||
- Introduces FTUE personalisation complete screen along with confetti celebration ([#5389](https://github.com/vector-im/element-android/issues/5389))
|
||||
|
||||
SDK API changes ⚠️
|
||||
------------------
|
||||
- Adds support for MSC3440, additional threads homeserver capabilities ([#5271](https://github.com/vector-im/element-android/issues/5271))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Refactoring for safer olm and megolm session usage ([#5380](https://github.com/vector-im/element-android/issues/5380))
|
||||
- Improve headers UI in Rooms/Messages lists ([#4533](https://github.com/vector-im/element-android/issues/4533))
|
||||
- Number of unread messages on space badge now include number of unread DMs ([#5260](https://github.com/vector-im/element-android/issues/5260))
|
||||
- Amend spaces menu to be consistent with iOS version ([#5270](https://github.com/vector-im/element-android/issues/5270))
|
||||
- Selected space highlight changed in left panel ([#5346](https://github.com/vector-im/element-android/issues/5346))
|
||||
- [Rooms list] Do not suggest collapse the unique section ([#5347](https://github.com/vector-im/element-android/issues/5347))
|
||||
- Add analytics support for threads ([#5378](https://github.com/vector-im/element-android/issues/5378))
|
||||
- Add top margin before our first message ([#5384](https://github.com/vector-im/element-android/issues/5384))
|
||||
- Improved onboarding registration unit test coverage ([#5408](https://github.com/vector-im/element-android/issues/5408))
|
||||
- Adds stable room hierarchy endpoint with a fallback to the unstable one ([#5443](https://github.com/vector-im/element-android/issues/5443))
|
||||
- Use ColorPrimary for attachmentGalleryButton tint ([#5501](https://github.com/vector-im/element-android/issues/5501))
|
||||
- Added online presence indicator attribute online to match offline styling ([#5513](https://github.com/vector-im/element-android/issues/5513))
|
||||
- Add a presence sync enabling build config ([#5563](https://github.com/vector-im/element-android/issues/5563))
|
||||
- Show stickers on click ([#5572](https://github.com/vector-im/element-android/issues/5572))
|
||||
|
||||
|
||||
Changes in Element v1.4.4 (2022-03-09)
|
||||
======================================
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ ext.libs = [
|
|||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||
'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
|
||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40104060.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40104060.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.6
|
2
fastlane/metadata/android/en-US/changelogs/40104070.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40104070.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: Various bug fixes and stability improvements.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.7
|
2
fastlane/metadata/android/en-US/changelogs/40104080.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40104080.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/es-ES/changelogs/40104000.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/40104000.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Principales cambios de esta versión: primera implementación de los hilos de mensajes. Burbujas de mensajes.
|
||||
Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.0
|
2
fastlane/metadata/android/es-ES/changelogs/40104020.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/40104020.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Principales cambios de esta versión: añadir @room, encuestas cerradas y muchos cambios menores más.
|
||||
Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.2
|
2
fastlane/metadata/android/fa/changelogs/40104000.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40104000.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
تغییرات اصلی در این نگارش: پیاده سازی نخستین پیامهای رشتهای. حبابهای پیام.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.0
|
2
fastlane/metadata/android/fa/changelogs/40104020.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40104020.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
تغییرات اصلی در این نگارش: افزودن پشتیبانی به @room و نظرسنجیهای فاش نشده در کنار تغییرات کوچک دیگر.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.2
|
|
@ -8,12 +8,13 @@ Az Element egy biztonságos üzenetküldő, és egy csapatmunka app, amely távo
|
|||
- Videochat, VoIP, és képernyőmegosztási lehetőséggel
|
||||
- Egyszerű integráció a kedvenc online kollaborációs eszközeiddel, projektkezelési eszközökkel, VoIP szolgáltatásokkal, és más csoportos üzenetküldő alkalmazásokkal
|
||||
|
||||
Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages.
|
||||
Az Element teljesen más, mint az összes többi üzenetküldő és kollaborációs alkalmazás. A biztonságos üzenetküldést és decentralizált kommunikációt biztosító Matrix platformot használja. Akár egyénileg üzemeltetett szervereket is lehet használni az adatok teljes kontrollálása érdekében.
|
||||
|
||||
<b>Privacy and encrypted messaging</b>
|
||||
Element protects you from unwanted ads, data mining and walled gardens. It also secures all your data, one-to-one video and voice communication through end-to-end encryption and cross-signed device verification.
|
||||
<b>Magánszféra és titkosított csevegés</b>
|
||||
Az Element megvéd a nemkívánatos hirdetésektől, adatbányászattól, és a zárt platformoktól. Ezeken felül biztonságban tartja az összes adatod és 1:1 hívásod a végponti titkosításnak és az eszközök-közti hitelesítésnek köszönhetően.
|
||||
|
||||
Az Element átadja neked az irányítást a magánszférád felett, miközben lehetővé teszi, hogy biztonságosan kommunikálj bárkivel a Matrix hálózatban, vagy a többi üzleti kommunikációs eszközt használókkal, az olyan appok integrálásának köszönhetően, mint például a Slack.
|
||||
|
||||
Element gives you control over your privacy while allowing you to communicate securely with anyone on the Matrix network, or other business collaboration tools by integrating with apps such as Slack.
|
||||
|
||||
<b>Element can be self-hosted</b>
|
||||
To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility.
|
||||
|
|
2
fastlane/metadata/android/ru-RU/changelogs/40104000.txt
Normal file
2
fastlane/metadata/android/ru-RU/changelogs/40104000.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Основные изменения в этой версии: Начальная реализация веток сообщений. Сообщения пузыри.
|
||||
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.0
|
2
fastlane/metadata/android/ru-RU/changelogs/40104020.txt
Normal file
2
fastlane/metadata/android/ru-RU/changelogs/40104020.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Основные изменения в этой версии: добавлена поддержка @room и нераскрытых опросов, а также множество других мелких изменений.
|
||||
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.2
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
distributionSha256Sum=a9a7b7baba105f6557c9dcf9c3c6e8f7e57e6b49889c5f1d133f015d0727e4be
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -59,7 +59,7 @@ dependencies {
|
|||
implementation libs.jetbrains.coroutinesCore
|
||||
implementation libs.jetbrains.coroutinesAndroid
|
||||
|
||||
testImplementation 'org.json:json:20211205'
|
||||
testImplementation 'org.json:json:20220320'
|
||||
testImplementation libs.tests.junit
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espressoCore
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.ContextMenu
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
@ -77,10 +76,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
|
|||
menuInfo: ContextMenu.ContextMenuInfo?
|
||||
) {
|
||||
if (copyValue != null) {
|
||||
val menuItem = menu?.add(
|
||||
Menu.NONE, R.id.copy_value,
|
||||
Menu.NONE, R.string.copy_value
|
||||
)
|
||||
val menuItem = menu?.add(R.string.copy_value)
|
||||
val clipService =
|
||||
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
menuItem?.setOnMenuItemClickListener {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/copy_value"
|
||||
android:title="@string/copy_value" />
|
||||
|
||||
</menu>
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="copy_value">Copy Value</string>
|
||||
</resources>
|
||||
|
|
|
@ -71,19 +71,6 @@
|
|||
android:enabled="false"
|
||||
android:text="Destructive disabled" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Unelevated.Bot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Bot" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Unelevated.Bot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="Bot disabled" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -98,19 +85,6 @@
|
|||
android:enabled="false"
|
||||
android:text="Outline disabled" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Poll " />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="Poll disabled" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
android:padding="16dp">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Dark"
|
||||
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Light"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Continue with XXX" />
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#4285F4" android:state_enabled="true"/>
|
||||
<item android:color="@color/vctr_disabled_view_color_light" android:state_enabled="false"/>
|
||||
<item android:color="#3367D6" android:state_pressed="true"/>
|
||||
<item android:color="#4285F4" android:state_focused="true"/>
|
||||
</selector>
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="DialpadKeyNumberStyle">
|
||||
<item name="android:textColor">?attr/vctr_content_primary</item>
|
||||
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
<style name="DialpadKeyLettersStyle">
|
||||
<item name="android:textColor">?attr/vctr_content_secondary</item>
|
||||
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
|
||||
<item name="android:fontFamily">sans-serif-regular</item>
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:gravity">center_horizontal</item>
|
||||
</style>
|
||||
|
||||
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
|
||||
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
|
||||
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
|
||||
</style>
|
||||
|
||||
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
|
||||
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
|
||||
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -46,8 +46,6 @@
|
|||
<attr name="conference_animation_from" format="color" />
|
||||
<attr name="conference_animation_to" format="color" />
|
||||
|
||||
<attr name="vctr_presence_indicator_online" format="color" />
|
||||
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -9,10 +9,6 @@
|
|||
<color name="soft_resource_limit_exceeded">#2f9edb</color>
|
||||
<color name="hard_resource_limit_exceeded">?colorError</color>
|
||||
|
||||
<!-- Button color -->
|
||||
<color name="button_bot_background_color">#14368BD6</color>
|
||||
<color name="button_bot_enabled_text_color">@color/palette_azure</color>
|
||||
|
||||
<!-- Notification (do not depends on theme) -->
|
||||
<color name="notification_accent_color">@color/palette_azure</color>
|
||||
<color name="key_share_req_accent_color">@color/palette_melon</color>
|
||||
|
@ -22,7 +18,6 @@
|
|||
<color name="bg_call_screen_blur">#99000000</color>
|
||||
<color name="bg_call_screen">#27303A</color>
|
||||
|
||||
<color name="vctr_notice_secondary">#FF61708B</color>
|
||||
<color name="vctr_notice_secondary_alpha12">#1E61708B</color>
|
||||
|
||||
<!-- Other useful color -->
|
||||
|
@ -86,16 +81,6 @@
|
|||
<color name="vctr_touch_guard_bg_dark">#BF000000</color>
|
||||
<color name="vctr_touch_guard_bg_black">#BF000000</color>
|
||||
|
||||
<attr name="vctr_attachment_selector_background" format="color" />
|
||||
<color name="vctr_attachment_selector_background_light">#FFFFFFFF</color>
|
||||
<color name="vctr_attachment_selector_background_dark">#FF22262E</color>
|
||||
<color name="vctr_attachment_selector_background_black">#FF090A0C</color>
|
||||
|
||||
<attr name="vctr_attachment_selector_border" format="color" />
|
||||
<color name="vctr_attachment_selector_border_light">#FFE9EDF1</color>
|
||||
<color name="vctr_attachment_selector_border_dark">#FF22262E</color>
|
||||
<color name="vctr_attachment_selector_border_black">#FF090A0C</color>
|
||||
|
||||
<attr name="vctr_room_active_widgets_banner_bg" format="color" />
|
||||
<color name="vctr_room_active_widgets_banner_bg_light">#EBEFF5</color>
|
||||
<color name="vctr_room_active_widgets_banner_bg_dark">#27303A</color>
|
||||
|
@ -110,9 +95,7 @@
|
|||
<color name="vctr_waiting_background_color_light">#AAAAAAAA</color>
|
||||
<color name="vctr_waiting_background_color_dark">#55555555</color>
|
||||
|
||||
<attr name="vctr_disabled_view_color" format="color" />
|
||||
<color name="vctr_disabled_view_color_light">#EEEEEE</color>
|
||||
<color name="vctr_disabled_view_color_dark">#61708B</color>
|
||||
|
||||
<attr name="vctr_reaction_background_off" format="color" />
|
||||
<color name="vctr_reaction_background_off_light">#FFF3F8FD</color>
|
||||
|
@ -142,4 +125,18 @@
|
|||
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
||||
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
||||
|
||||
<attr name="vctr_presence_indicator_online" format="color" />
|
||||
<color name="vctr_presence_indicator_online_light">@color/palette_element_green</color>
|
||||
<color name="vctr_presence_indicator_online_dark">@color/palette_element_green</color>
|
||||
|
||||
<!-- Location sharing colors -->
|
||||
<attr name="vctr_live_location" format="color" />
|
||||
<color name="vctr_live_location_light">@color/palette_prune</color>
|
||||
<color name="vctr_live_location_dark">@color/palette_prune</color>
|
||||
|
||||
<!-- Shield colors -->
|
||||
<color name="shield_color_trust">#8BC34A</color>
|
||||
<color name="shield_color_black">#212121</color>
|
||||
<color name="shield_color_warning">#FF4B55</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -9,20 +9,11 @@
|
|||
<dimen name="layout_vertical_margin_big">32dp</dimen>
|
||||
|
||||
<dimen name="profile_avatar_size">50dp</dimen>
|
||||
<dimen name="floating_action_button_margin">16dp</dimen>
|
||||
<dimen name="navigation_view_height">196dp</dimen>
|
||||
<dimen name="navigation_avatar_top_margin">44dp</dimen>
|
||||
<dimen name="item_decoration_left_margin">72dp</dimen>
|
||||
<dimen name="item_event_message_state_size">16dp</dimen>
|
||||
|
||||
<dimen name="item_event_message_media_button_size">32dp</dimen>
|
||||
|
||||
<dimen name="chat_avatar_size">40dp</dimen>
|
||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
||||
|
||||
<dimen name="quote_width">4dp</dimen>
|
||||
<dimen name="quote_gap">8dp</dimen>
|
||||
<dimen name="drawable_padding_small">8dp</dimen>
|
||||
|
||||
<item name="dialog_width_ratio" format="float" type="dimen">0.75</item>
|
||||
|
||||
|
@ -74,4 +65,10 @@
|
|||
|
||||
<item name="ftue_auth_profile_picture_height" format="float" type="dimen">0.15</item>
|
||||
<item name="ftue_auth_profile_picture_icon_height" format="float" type="dimen">0.05</item>
|
||||
|
||||
<!-- Location sharing -->
|
||||
<dimen name="location_sharing_option_default_padding">10dp</dimen>
|
||||
<dimen name="location_sharing_locate_button_margin_vertical">16dp</dimen>
|
||||
<dimen name="location_sharing_locate_button_margin_horizontal">12dp</dimen>
|
||||
<dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
|
||||
|
||||
<!-- Define all the colors used across the Element Android project
|
||||
Source: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1338%3A17947 -->
|
||||
<!--
|
||||
Define all the colors used across the Element Android project
|
||||
Source: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1338%3A17947
|
||||
Some colors are not used, but we want the palette to be complete so we ignore the lint error
|
||||
UnusedResources for all the resources in this file
|
||||
-->
|
||||
|
||||
<!-- For all themes -->
|
||||
<color name="palette_azure">#368BD6</color>
|
||||
|
@ -15,6 +19,7 @@
|
|||
<color name="palette_element_green">#0DBD8B</color>
|
||||
<color name="palette_white">#FFFFFF</color>
|
||||
<color name="palette_vermilion">#FF5B55</color>
|
||||
<!-- (unused) -->
|
||||
<color name="palette_ems">#7E69FF</color>
|
||||
<color name="palette_aqua">#2DC2C5</color>
|
||||
<color name="palette_prune">#5C56F5</color>
|
||||
|
@ -27,6 +32,7 @@
|
|||
<color name="palette_gray_150">#8D97A5</color>
|
||||
<color name="palette_gray_200">#737D8C</color>
|
||||
<color name="palette_black_900">#17191C</color>
|
||||
<!-- (unused) -->
|
||||
<color name="palette_ice">#F4F9FD</color>
|
||||
|
||||
<!-- For dark themes -->
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
<attr name="vctr_system" format="color" />
|
||||
<color name="element_system_light">@color/palette_gray_25</color>
|
||||
<color name="element_system_dark">@color/palette_black_950</color>
|
||||
<color name="element_system_black">@color/palette_black_950</color>
|
||||
|
||||
<color name="element_background_light">@color/palette_white</color>
|
||||
<color name="element_background_dark">@color/palette_black_800</color>
|
||||
|
@ -54,5 +53,4 @@
|
|||
<color name="element_room_01">@color/palette_verde</color>
|
||||
<color name="element_room_02">@color/palette_azure</color>
|
||||
<color name="element_room_03">@color/palette_grape</color>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="LocationSharingOptionView">
|
||||
<attr name="locShareIcon" format="reference" />
|
||||
<attr name="locShareIconBackground" format="reference" />
|
||||
<attr name="locShareIconBackgroundTint" format="color" />
|
||||
<attr name="locShareIconPadding" format="dimension" />
|
||||
<attr name="locShareIconDescription" format="string" />
|
||||
<attr name="locShareTitle" format="string" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="MapTilerMapView">
|
||||
<attr name="showLocateButton" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AttachmentTypeSelectorButton">
|
||||
<item name="android:layout_width">56dp</item>
|
||||
<item name="android:layout_height">56dp</item>
|
||||
<item name="android:scaleType">center</item>
|
||||
</style>
|
||||
|
||||
<style name="AttachmentTypeSelectorLabel">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:layout_marginTop">8dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -32,23 +32,6 @@
|
|||
<!-- Keep default colors from the theme -->
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Unelevated" parent="Widget.MaterialComponents.Button.UnelevatedButton">
|
||||
<item name="android:paddingLeft">16dp</item>
|
||||
<item name="android:paddingRight">16dp</item>
|
||||
<item name="android:minWidth">94dp</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
|
||||
<item name="lineHeight">24sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Unelevated.Bot">
|
||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayBot</item>
|
||||
</style>
|
||||
|
||||
<style name="VectorMaterialThemeOverlayBot">
|
||||
<item name="colorPrimary">@color/button_bot_background_color</item>
|
||||
<item name="colorOnPrimary">@color/button_bot_enabled_text_color</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Text" parent="Widget.MaterialComponents.Button.TextButton">
|
||||
<item name="colorControlHighlight">?colorSecondary</item>
|
||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item>
|
||||
|
@ -79,9 +62,4 @@
|
|||
<item name="colorPrimary">?colorOnPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.Poll">
|
||||
<item name="android:minHeight">44dp</item>
|
||||
<item name="cornerRadius">10dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
|
||||
|
||||
<!-- This file contain styles to override the style from the library android-dialer-1.2.5.aar
|
||||
This is why we ignore the lint error UnusedResources -->
|
||||
|
||||
<style name="DialpadKeyNumberStyle">
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
|
|
11
library/ui-styles/src/main/res/values/styles_location.xml
Normal file
11
library/ui-styles/src/main/res/values/styles_location.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive">
|
||||
<item name="android:background">?selectableItemBackground</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -28,11 +28,6 @@
|
|||
<item name="android:textColor">@color/black_54</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Google.Dark">
|
||||
<item name="android:backgroundTint">@color/button_social_google_background_selector_dark</item>
|
||||
<item name="android:textColor">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Github">
|
||||
<item name="icon">@drawable/ic_social_github</item>
|
||||
</style>
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_black</item>
|
||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_black</item>
|
||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_black</item>
|
||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_black</item>
|
||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_black</item>
|
||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_black</item>
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_dark</item>
|
||||
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_dark</item>
|
||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_dark</item>
|
||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_dark</item>
|
||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_dark</item>
|
||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_dark</item>
|
||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_dark</item>
|
||||
|
@ -45,6 +43,7 @@
|
|||
|
||||
<!-- Presence Indicator colors -->
|
||||
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
|
||||
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_dark</item>
|
||||
|
||||
<!-- Some aliases -->
|
||||
<item name="vctr_header_background">?vctr_system</item>
|
||||
|
@ -183,6 +182,8 @@
|
|||
|
||||
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
||||
|
||||
<!-- Location sharing -->
|
||||
<item name="vctr_live_location">@color/vctr_live_location_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_light</item>
|
||||
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_light</item>
|
||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_light</item>
|
||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_light</item>
|
||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_light</item>
|
||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_light</item>
|
||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_light</item>
|
||||
|
@ -45,6 +43,7 @@
|
|||
|
||||
<!-- Presence Indicator colors -->
|
||||
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
|
||||
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_light</item>
|
||||
|
||||
<!-- Some aliases -->
|
||||
<item name="vctr_header_background">?vctr_system</item>
|
||||
|
@ -184,6 +183,8 @@
|
|||
|
||||
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
||||
|
||||
<!-- Location sharing -->
|
||||
<item name="vctr_live_location">@color/vctr_live_location_light</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
<item name="vctr_fab_label_color">@android:color/white</item>
|
||||
<item name="vctr_touch_guard_bg">@color/background_touch_guard_sc</item>
|
||||
<item name="vctr_keys_backup_banner_accent_color">@color/background_floating_sc</item>
|
||||
<item name="vctr_attachment_selector_background">?colorBackgroundFloating</item>
|
||||
<item name="vctr_attachment_selector_border">@color/list_divider_color_sc</item>
|
||||
<item name="vctr_room_active_widgets_banner_bg">@color/background_floating_sc</item>
|
||||
<item name="vctr_room_active_widgets_banner_text">?vctr_content_secondary</item>
|
||||
<item name="vctr_reaction_background_off">@color/background_sc</item>
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
<item name="vctr_fab_label_color">?vctr_content_primary</item>
|
||||
<item name="vctr_touch_guard_bg">@color/background_touch_guard_sc_light</item>
|
||||
<item name="vctr_keys_backup_banner_accent_color">@color/background_floating_sc_light</item>
|
||||
<item name="vctr_attachment_selector_background">?colorBackgroundFloating</item>
|
||||
<item name="vctr_attachment_selector_border">@color/list_divider_color_sc_light</item>
|
||||
<item name="vctr_room_active_widgets_banner_bg">@color/background_floating_sc_light</item>
|
||||
<item name="vctr_room_active_widgets_banner_text">?vctr_content_secondary</item>
|
||||
<item name="vctr_reaction_background_off">@color/background_sc_light_secondary</item>
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
|
@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
|
|||
return room.getLiveRoomNotificationState().asFlow()
|
||||
}
|
||||
|
||||
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
|
||||
return room.getAllThreadSummariesLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
room.getAllThreadSummaries()
|
||||
}
|
||||
}
|
||||
fun liveThreadList(): Flow<List<ThreadRootEvent>> {
|
||||
return room.getAllThreadsLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
room.getAllThreads()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
|
||||
return room.getMarkedThreadNotificationsLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
|
|
|
@ -31,12 +31,11 @@ android {
|
|||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.4.4\""
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.4.8\""
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
||||
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||
buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\""
|
||||
|
||||
defaultConfig {
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
|
@ -167,7 +166,7 @@ dependencies {
|
|||
implementation libs.apache.commonsImaging
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45'
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
testImplementation 'org.robolectric:robolectric:4.7.3'
|
||||
|
@ -175,7 +174,7 @@ dependencies {
|
|||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
testImplementation libs.mockk.mockk
|
||||
testImplementation libs.tests.kluent
|
||||
implementation libs.jetbrains.coroutinesAndroid
|
||||
testImplementation libs.jetbrains.coroutinesTest
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
// Transitively required for mocking realm as monarchy doesn't expose Rx
|
||||
|
|
|
@ -23,7 +23,7 @@ object TestConstants {
|
|||
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
||||
|
||||
// Time out to use when waiting for server response.
|
||||
private const val AWAIT_TIME_OUT_MILLIS = 30_000
|
||||
private const val AWAIT_TIME_OUT_MILLIS = 60_000
|
||||
|
||||
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
||||
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
||||
|
|
|
@ -0,0 +1,649 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import kotlinx.coroutines.delay
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class E2eeSanityTests : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
/**
|
||||
* Simple test that create an e2ee room.
|
||||
* Some new members are added, and a message is sent.
|
||||
* We check that the message is e2e and can be decrypted.
|
||||
*
|
||||
* Additional users join, we check that they can't decrypt history
|
||||
*
|
||||
* Alice sends a new message, then check that the new one can be decrypted
|
||||
*/
|
||||
@Test
|
||||
fun testSendingE2EEMessages() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
// add some more users and invite them
|
||||
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
||||
.map {
|
||||
testHelper.createAccount(it, SessionTestParams(true))
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "All accounts created")
|
||||
// we want to invite them in the room
|
||||
otherAccounts.forEach {
|
||||
testHelper.runBlockingTest {
|
||||
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
|
||||
aliceRoomPOV.invite(it.myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
// All user should accept invite
|
||||
otherAccounts.forEach { otherSession ->
|
||||
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||
}
|
||||
|
||||
// check that alice see them as joined (not really necessary?)
|
||||
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
|
||||
|
||||
Log.v("#E2E TEST", "All users have joined the room")
|
||||
Log.v("#E2E TEST", "Alice is sending the message")
|
||||
|
||||
val text = "This is my message"
|
||||
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
|
||||
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
||||
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||
|
||||
// All should be able to decrypt
|
||||
otherAccounts.forEach { otherSession ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new user to the room, and check that he can't decrypt
|
||||
val newAccount = listOf("adam") // , "adam", "manu")
|
||||
.map {
|
||||
testHelper.createAccount(it, SessionTestParams(true))
|
||||
}
|
||||
|
||||
newAccount.forEach {
|
||||
testHelper.runBlockingTest {
|
||||
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
|
||||
aliceRoomPOV.invite(it.myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
newAccount.forEach {
|
||||
waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
||||
}
|
||||
|
||||
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(3_000)
|
||||
}
|
||||
|
||||
// check that messages are encrypted (uisi)
|
||||
newAccount.forEach { otherSession ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
|
||||
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
|
||||
}
|
||||
timelineEvent != null &&
|
||||
timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
|
||||
timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let alice send a new message
|
||||
Log.v("#E2E TEST", "Alice sends a new message")
|
||||
|
||||
val secondMessage = "2 This is my message"
|
||||
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
|
||||
|
||||
// new members should be able to decrypt it
|
||||
newAccount.forEach { otherSession ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
|
||||
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
|
||||
}
|
||||
timelineEvent != null &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
otherAccounts.forEach {
|
||||
testHelper.signOutAndClose(it)
|
||||
}
|
||||
newAccount.forEach { testHelper.signOutAndClose(it) }
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick test for basic key backup
|
||||
* 1. Create e2e between Alice and Bob
|
||||
* 2. Alice sends 3 messages, using 3 different sessions
|
||||
* 3. Ensure bob can decrypt
|
||||
* 4. Create backup for bob and upload keys
|
||||
*
|
||||
* 5. Sign out alice and bob to ensure no gossiping will happen
|
||||
*
|
||||
* 6. Let bob sign in with a new session
|
||||
* 7. Ensure history is UISI
|
||||
* 8. Import backup
|
||||
* 9. Check that new session can decrypt
|
||||
*/
|
||||
@Test
|
||||
fun testBasicBackupImport() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
Log.v("#E2E TEST", "Create and start key backup for bob ...")
|
||||
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
|
||||
val keyBackupPassword = "FooBarBaz"
|
||||
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
|
||||
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
||||
}
|
||||
val version = testHelper.doSync<KeysVersion> {
|
||||
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||
}
|
||||
Log.v("#E2E TEST", "... Key backup started and enabled for bob")
|
||||
// Bob session should now have
|
||||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
// let's send a few message to bob
|
||||
val sentEventIds = mutableListOf<String>()
|
||||
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
// we want more so let's discard the session
|
||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||
|
||||
testHelper.runBlockingTest {
|
||||
delay(1_000)
|
||||
}
|
||||
}
|
||||
Log.v("#E2E TEST", "Bob received all and can decrypt")
|
||||
|
||||
// Let's wait a bit to be sure that bob has backed up the session
|
||||
|
||||
Log.v("#E2E TEST", "Force key backup for Bob...")
|
||||
testHelper.waitWithLatch { latch ->
|
||||
bobKeysBackupService.backupAllGroupSessions(
|
||||
null,
|
||||
TestMatrixCallback(latch, true)
|
||||
)
|
||||
}
|
||||
Log.v("#E2E TEST", "... Key backup done for Bob")
|
||||
|
||||
// Now lets logout both alice and bob to ensure that we won't have any gossiping
|
||||
|
||||
val bobUserId = bobSession.myUserId
|
||||
Log.v("#E2E TEST", "Logout alice and bob...")
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSession)
|
||||
Log.v("#E2E TEST", "..Logout alice and bob...")
|
||||
|
||||
testHelper.runBlockingTest {
|
||||
delay(1_000)
|
||||
}
|
||||
|
||||
// Create a new session for bob
|
||||
Log.v("#E2E TEST", "Create a new session for Bob")
|
||||
val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
|
||||
|
||||
// check that bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that bob can't currently decrypt")
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
|
||||
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
|
||||
}
|
||||
timelineEvent != null &&
|
||||
timelineEvent.root.getClearType() == EventType.ENCRYPTED
|
||||
}
|
||||
}
|
||||
}
|
||||
// after initial sync events are not decrypted, so we have to try manually
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
|
||||
// Let's now import keys from backup
|
||||
|
||||
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
||||
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||
keysBackupService.getVersion(version.version, it)
|
||||
}
|
||||
|
||||
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||
keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
|
||||
keyBackupPassword,
|
||||
null,
|
||||
null,
|
||||
null, it)
|
||||
}
|
||||
|
||||
assertEquals(3, importedResult.totalNumberOfKeys)
|
||||
}
|
||||
|
||||
// ensure bob can now decrypt
|
||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a new verified session that was not supposed to get the keys initially will
|
||||
* get them from an older one.
|
||||
*/
|
||||
@Test
|
||||
fun testSimpleGossip() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
cryptoTestHelper.initializeCrossSigning(bobSession)
|
||||
|
||||
// let's send a few message to bob
|
||||
val sentEventIds = mutableListOf<String>()
|
||||
val messagesText = listOf("1. Hello", "2. Bob")
|
||||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure bob can decrypt
|
||||
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
|
||||
|
||||
// Let's now add a new bob session
|
||||
// Create a new session for bob
|
||||
Log.v("#E2E TEST", "Create a new session for Bob")
|
||||
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
|
||||
// check that new bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
|
||||
// Try to request
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||
newBobSession.cryptoService().requestRoomKeyForEvent(event)
|
||||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
|
||||
|
||||
// Now mark new bob session as verified
|
||||
|
||||
bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
|
||||
newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
|
||||
|
||||
// now let new session re-request
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that if a better key is forwarded (lower index, it is then used)
|
||||
*/
|
||||
@Test
|
||||
fun testForwardBetterKey() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
|
||||
|
||||
// let's send a few message to bob
|
||||
var firstEventId: String
|
||||
val firstMessage = "1. Hello"
|
||||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
firstMessage.let { text ->
|
||||
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure bob can decrypt
|
||||
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||
|
||||
// Let's add a new unverified session from bob
|
||||
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
||||
|
||||
// check that new bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
|
||||
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
||||
var secondEventId: String
|
||||
val secondMessage = "2. New Device?"
|
||||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
secondMessage.let { text ->
|
||||
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check that both messages have same sessionId (it's just that we don't have index 0)
|
||||
val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
|
||||
val firstSessionId = firstEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
val secondSessionId = secondEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
|
||||
|
||||
// Confirm we can decrypt one but not the other
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
fail("Should not be able to decrypt event")
|
||||
} catch (error: MXCryptoError) {
|
||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt event")
|
||||
}
|
||||
}
|
||||
|
||||
// Now let's verify bobs session, and re-request keys
|
||||
bobSessionWithBetterKey.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
|
||||
|
||||
newBobSession.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
||||
|
||||
// now let new session request
|
||||
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
// old session should have shared the key at earliest known index now
|
||||
// we should be able to decrypt both
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt first event now $error")
|
||||
}
|
||||
}
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt event $error")
|
||||
}
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
||||
aliceRoomPOV.sendTextMessage(text)
|
||||
var sentEventId: String? = null
|
||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val decryptedMsg = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.also { list ->
|
||||
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
||||
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||
}
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
||||
sentEventId = decryptedMsg?.eventId
|
||||
decryptedMsg != null
|
||||
}
|
||||
|
||||
timeline.dispose()
|
||||
}
|
||||
return sentEventId
|
||||
}
|
||||
|
||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
otherAccounts.map {
|
||||
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
|
||||
}.all {
|
||||
it == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.runBlockingTest(60_000) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||
try {
|
||||
otherSession.joinRoom(e2eRoomID)
|
||||
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
|
||||
// it's ok we will wait after
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||
roomSummary != null && roomSummary.membership == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
||||
sentEventIds.forEachIndexed { index, sentEventId ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
session.cryptoService().decryptEvent(event, "").let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
}
|
||||
} catch (error: MXCryptoError) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
event.getClearType() == EventType.MESSAGE &&
|
||||
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(event, "")
|
||||
fail("Should not be able to decrypt event")
|
||||
} catch (error: MXCryptoError) {
|
||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||
if (expectedError == null) {
|
||||
Assert.assertNotNull(errorType)
|
||||
} else {
|
||||
assertEquals(expectedError, errorType, "Message expected to be UISI")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun ensure_outbound_session_happy_path() {
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val e2eRoomID = testData.roomId
|
||||
|
@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
// Just send a real message as test
|
||||
val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
|
||||
|
||||
assertEquals(megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId, "Unexpected megolm session")
|
||||
assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId,)
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
|
|||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest {
|
|||
* -> This is automatically fixed after SDKs restarted the olm session
|
||||
*/
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun testUnwedging() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
|
@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest {
|
|||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
|
||||
// bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
// aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
|
||||
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest {
|
|||
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
|
||||
|
||||
aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
|
||||
olmDevice.clearOlmSessionCache()
|
||||
Thread.sleep(6_000)
|
||||
|
||||
// Force new session, and key share
|
||||
|
@ -227,8 +224,10 @@ class UnwedgingTest : InstrumentedTest {
|
|||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
// we should get back the key and be able to decrypt
|
||||
val result = tryOrNull {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
val result = testHelper.runBlockingTest {
|
||||
tryOrNull {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
}
|
||||
}
|
||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
||||
result != null
|
||||
|
|
|
@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
assert(receivedEvent!!.isEncrypted())
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
commonTestHelper.runBlockingTest {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
fail("should fail")
|
||||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
commonTestHelper.runBlockingTest {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
fail("should fail")
|
||||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
commonTestHelper.runBlockingTest {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
fail("should have been able to decrypt")
|
||||
}
|
||||
|
@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest {
|
|||
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
||||
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
||||
|
||||
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
|
||||
var dRes = tryOrNull {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
|
||||
}
|
||||
}
|
||||
|
||||
assert(dRes == null)
|
||||
|
||||
|
@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest {
|
|||
Thread.sleep(3_000)
|
||||
|
||||
// With the bug the first session would have improperly reshare that key :/
|
||||
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
|
||||
dRes = tryOrNull {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
|
||||
}
|
||||
}
|
||||
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
|
||||
assert(dRes?.clearEvent == null)
|
||||
}
|
||||
|
|
|
@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest {
|
|||
// Bob should not be able to decrypt because the keys is withheld
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
testHelper.runBlockingTest {
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
}
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
|
@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest {
|
|||
// Previous message should still be undecryptable (partially withheld session)
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
testHelper.runBlockingTest {
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
}
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
|
@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest {
|
|||
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||
testHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||
}
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
|
@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest {
|
|||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
|
||||
// try to decrypt and force key request
|
||||
tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||
tryOrNull {
|
||||
testHelper.runBlockingTest {
|
||||
bobSecondSession.cryptoService().decryptEvent(it.root, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||
timeLineEvent != null
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -39,6 +40,7 @@ import kotlin.coroutines.resume
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@Ignore("This test is flaky ; see issue #5449")
|
||||
class VerificationTest : InstrumentedTest {
|
||||
|
||||
data class ExpectedResult(
|
||||
|
|
|
@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
applicationFlavor = "TestFlavor",
|
||||
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
|
||||
)
|
||||
))
|
||||
),
|
||||
TestPermalinkService()
|
||||
)
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
|
||||
|
||||
class TestPermalinkService : PermalinkService {
|
||||
override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getLinkedId(url: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
||||
return when (type) {
|
||||
HTML -> "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
|
||||
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.timelineEvents.size shouldBeEqualTo 1
|
||||
}
|
||||
}
|
||||
|
@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.timelineEvents.size shouldBeEqualTo 1
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
|
||||
addTimelineEvent(
|
||||
roomId = roomId,
|
||||
eventEntity = fakeEvent,
|
||||
direction = direction,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.session.room.timeline
|
||||
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeGreaterThan
|
||||
import org.amshove.kluent.shouldContain
|
||||
import org.amshove.kluent.shouldContainAll
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class PollAggregationTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testAllPollUseCases() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val roomFromBobPOV = cryptoTestData.secondSession!!.getRoom(cryptoTestData.roomId)!!
|
||||
// Bob creates a poll
|
||||
roomFromBobPOV.sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
|
||||
|
||||
aliceSession.startSync(true)
|
||||
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
|
||||
aliceTimeline.start()
|
||||
|
||||
val TOTAL_TEST_COUNT = 7
|
||||
val lock = CountDownLatch(TOTAL_TEST_COUNT)
|
||||
|
||||
val aliceEventsListener = object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
|
||||
val pollEventId = pollEvent.eventId
|
||||
val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
|
||||
val pollSummary = pollEvent.annotations?.pollResponseSummary
|
||||
|
||||
if (pollContent == null) {
|
||||
fail("Poll content is null")
|
||||
return
|
||||
}
|
||||
|
||||
when (lock.count.toInt()) {
|
||||
TOTAL_TEST_COUNT -> {
|
||||
// Poll has just been created.
|
||||
testInitialPollConditions(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 1 -> {
|
||||
// Bob: Option 1
|
||||
testBobVotesOption1(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 2 -> {
|
||||
// Bob: Option 2
|
||||
testBobChangesVoteToOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 3 -> {
|
||||
// Alice: Option 2, Bob: Option 2
|
||||
testAliceAndBobVoteToOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 4 -> {
|
||||
// Alice: Option 1, Bob: Option 2
|
||||
testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.endPoll(pollEventId)
|
||||
}
|
||||
TOTAL_TEST_COUNT - 5 -> {
|
||||
// Alice: Option 1, Bob: Option 2 [poll is ended]
|
||||
testEndedPoll(pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 6 -> {
|
||||
// Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
|
||||
testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
|
||||
testEndedPoll(pollSummary)
|
||||
lock.countDown()
|
||||
}
|
||||
else -> {
|
||||
fail("Lock count ${lock.count} didn't handled.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceSession.stopSync()
|
||||
aliceTimeline.dispose()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
|
||||
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
// No votes yet, poll summary should be null
|
||||
pollSummary shouldBe null
|
||||
// Question should be the same as intended
|
||||
pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
|
||||
// Options should be the same as intended
|
||||
pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
|
||||
answers.size shouldBeEqualTo pollOptions.size
|
||||
answers.map { it.getBestAnswer() } shouldContainAll pollOptions
|
||||
}
|
||||
}
|
||||
|
||||
private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
|
||||
// Check if the intended vote is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 1)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended vote is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 1)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended votes is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 2)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votes?.get(1)?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 2
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
|
||||
val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended votes is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 2)
|
||||
aggregatedContent.votes!!.map { it.option } shouldContain firstAnswerId
|
||||
aggregatedContent.votes!!.map { it.option } shouldContain secondAnswerId
|
||||
aggregatedContent.votesSummary?.get(firstAnswerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(secondAnswerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(firstAnswerId)?.percentage shouldBeEqualTo 0.5
|
||||
aggregatedContent.votesSummary?.get(secondAnswerId)?.percentage shouldBeEqualTo 0.5
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
|
||||
pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
|
||||
}
|
||||
|
||||
private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
|
||||
aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
|
||||
aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val pollQuestion = "Do you like creating polls?"
|
||||
val pollOptions = listOf("Yes", "Absolutely", "As long as tests pass")
|
||||
}
|
||||
}
|
|
@ -60,7 +60,15 @@ data class MatrixConfiguration(
|
|||
/**
|
||||
* RoomDisplayNameFallbackProvider to provide default room display name.
|
||||
*/
|
||||
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
|
||||
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
|
||||
/**
|
||||
* True to enable presence information sync (if available). False to disable regardless of server setting.
|
||||
*/
|
||||
val presenceSyncEnabled: Boolean = true,
|
||||
/**
|
||||
* Thread messages default enable/disabled value
|
||||
*/
|
||||
val threadMessagesEnabledDefault: Boolean = false,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
|
@ -121,7 +121,7 @@ interface CryptoService {
|
|||
fun discardOutboundSession(roomId: String)
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
|
||||
|
||||
|
|
|
@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class AggregatedRelations(
|
||||
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
|
||||
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
|
||||
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
|
||||
@Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
|
||||
)
|
||||
|
|
|
@ -201,7 +201,11 @@ data class Event(
|
|||
*/
|
||||
fun getDecryptedTextSummary(): String? {
|
||||
if (isRedacted()) return "Message Deleted"
|
||||
val text = getDecryptedValue() ?: return null
|
||||
val text = getDecryptedValue() ?: run {
|
||||
if (isPoll()) { return getPollQuestion() ?: "created a poll." }
|
||||
return null
|
||||
}
|
||||
|
||||
return when {
|
||||
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||
isFileMessage() -> "sent a file."
|
||||
|
@ -349,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
|
||||
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
|
||||
|
||||
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||
|
||||
|
@ -372,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
|||
* Returns the poll question or null otherwise
|
||||
*/
|
||||
fun Event.getPollQuestion(): String? =
|
||||
getPollContent()?.pollCreationInfo?.question?.question
|
||||
getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
|
||||
/**
|
||||
* Returns the relation content for a specific type or null otherwise
|
||||
|
@ -385,12 +389,12 @@ fun Event.isReply(): Boolean {
|
|||
}
|
||||
|
||||
fun Event.isReplyRenderedInThread(): Boolean {
|
||||
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
|
||||
return isReply() && getRelationContent()?.shouldRenderInThread() == true
|
||||
}
|
||||
|
||||
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
|
||||
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
|
||||
|
||||
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
|
||||
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
|
||||
|
||||
fun Event.isEdition(): Boolean {
|
||||
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
|
||||
|
@ -406,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
|
|||
fun Event.getPollContent(): MessagePollContent? {
|
||||
return content.toModel<MessagePollContent>()
|
||||
}
|
||||
|
||||
fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START
|
||||
|
|
|
@ -103,9 +103,9 @@ object EventType {
|
|||
const val REACTION = "m.reaction"
|
||||
|
||||
// Poll
|
||||
const val POLL_START = "org.matrix.msc3381.poll.start"
|
||||
const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
|
||||
const val POLL_END = "org.matrix.msc3381.poll.end"
|
||||
val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
|
||||
val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
|
||||
val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
|
||||
|
||||
// Unwedging
|
||||
internal const val DUMMY = "m.dummy"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.events.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LatestThreadUnsignedRelation(
|
||||
override val limited: Boolean? = false,
|
||||
override val count: Int? = 0,
|
||||
@Json(name = "latest_event")
|
||||
val event: Event? = null,
|
||||
@Json(name = "current_user_participated")
|
||||
val isUserParticipating: Boolean? = false
|
||||
|
||||
) : UnsignedRelationInfo
|
|
@ -30,7 +30,6 @@ object RelationType {
|
|||
|
||||
/** Lets you define an event which is a thread reply to an existing event.*/
|
||||
const val THREAD = "m.thread"
|
||||
const val IO_THREAD = "io.element.thread"
|
||||
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "org.matrix.response"
|
||||
|
|
|
@ -50,7 +50,11 @@ data class HomeServerCapabilities(
|
|||
* This capability describes the default and available room versions a server supports, and at what level of stability.
|
||||
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
|
||||
*/
|
||||
val roomVersions: RoomVersionCapabilities? = null
|
||||
val roomVersions: RoomVersionCapabilities? = null,
|
||||
/**
|
||||
* True if the home server support threading
|
||||
*/
|
||||
var canUseThreading: Boolean = false
|
||||
) {
|
||||
|
||||
enum class RoomCapabilitySupport {
|
||||
|
|
|
@ -28,6 +28,11 @@ interface PermalinkService {
|
|||
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
||||
}
|
||||
|
||||
enum class SpanTemplateType {
|
||||
HTML,
|
||||
MARKDOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a permalink for an event.
|
||||
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
|
||||
|
@ -80,4 +85,15 @@ interface PermalinkService {
|
|||
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
|
||||
*/
|
||||
fun getLinkedId(url: String): String?
|
||||
|
||||
/**
|
||||
* Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
|
||||
* Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||
*
|
||||
* @param type: type of template to create
|
||||
* @param forceMatrixTo whether we should force using matrix.to base URL
|
||||
*
|
||||
* @return the created template
|
||||
*/
|
||||
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
|
|||
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||
|
@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
|
|||
interface Room :
|
||||
TimelineService,
|
||||
ThreadsService,
|
||||
ThreadsLocalService,
|
||||
SendService,
|
||||
DraftService,
|
||||
ReadService,
|
||||
|
|
|
@ -216,6 +216,12 @@ interface RoomService {
|
|||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
||||
|
||||
/**
|
||||
* Return a LiveData on the number of rooms
|
||||
* @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
|
||||
*/
|
||||
fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int>
|
||||
|
||||
/**
|
||||
* TODO Doc
|
||||
*/
|
||||
|
|
|
@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
|||
|
||||
interface UpdatableLivePageResult {
|
||||
val livePagedList: LiveData<PagedList<RoomSummary>>
|
||||
|
||||
fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
|
||||
|
||||
val liveBoundaries: LiveData<ResultBoundaries>
|
||||
var queryParams: RoomSummaryQueryParams
|
||||
}
|
||||
|
||||
data class ResultBoundaries(
|
||||
|
|
|
@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationAsset(
|
||||
@Json(name = "type") val type: LocationAssetType? = null
|
||||
@Json(name = "type") val type: String? = null
|
||||
)
|
||||
|
|
|
@ -16,11 +16,20 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
/**
|
||||
* Define what particular asset is being referred to.
|
||||
* We don't use enum type since it is not limited to a specific set of values.
|
||||
* The way this type should be interpreted in client side is described in
|
||||
* [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||
*/
|
||||
object LocationAssetType {
|
||||
/**
|
||||
* Used for user location sharing.
|
||||
**/
|
||||
const val SELF = "m.self"
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class LocationAssetType {
|
||||
@Json(name = "m.self")
|
||||
SELF
|
||||
/**
|
||||
* Used for pin drop location sharing.
|
||||
**/
|
||||
const val PIN = "m.pin"
|
||||
}
|
||||
|
|
|
@ -39,37 +39,47 @@ data class MessageLocationContent(
|
|||
*/
|
||||
@Json(name = "geo_uri") val geoUri: String,
|
||||
|
||||
/**
|
||||
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
|
||||
/**
|
||||
* m.asset defines a generic asset that can be used for location tracking but also in other places like
|
||||
* inventories, geofencing, checkins/checkouts etc.
|
||||
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
||||
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
||||
* See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||
*/
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
|
||||
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
|
||||
/**
|
||||
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc1767.text") val text: String? = null
|
||||
@Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
|
||||
@Json(name = "m.ts") val ts: Long? = null,
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
|
||||
@Json(name = "m.text") val text: String? = null,
|
||||
/**
|
||||
* Defines a generic asset that can be used for location tracking but also in other places like
|
||||
* inventories, geofencing, checkins/checkouts etc.
|
||||
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
||||
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
||||
* See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||
fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
|
||||
|
||||
fun getBestTs() = ts ?: unstableTs
|
||||
|
||||
fun getBestText() = text ?: unstableText
|
||||
|
||||
fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
|
||||
|
||||
fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
|
||||
|
||||
/**
|
||||
* @return true if the location asset is a user location, not a generic one.
|
||||
*/
|
||||
fun isSelfLocation(): Boolean {
|
||||
// Should behave like m.self if locationAsset is null
|
||||
val locationAsset = getBestLocationAsset()
|
||||
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
|
||||
@Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollResponseContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
|
||||
@Json(name = "m.response") val response: PollResponse? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestResponse() = response ?: unstableResponse
|
||||
}
|
||||
|
|
|
@ -22,5 +22,9 @@ 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
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
|
||||
@Json(name = "m.text") val answer: String? = null
|
||||
) {
|
||||
|
||||
fun getBestAnswer() = answer ?: unstableAnswer
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollCreationInfo(
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
)
|
||||
|
|
|
@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollQuestion(
|
||||
@Json(name = "org.matrix.msc1767.text") val question: String? = null
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
|
||||
@Json(name = "m.text") val question: String? = null
|
||||
) {
|
||||
|
||||
fun getBestQuestion() = question ?: unstableQuestion
|
||||
}
|
||||
|
|
|
@ -25,11 +25,17 @@ enum class PollType {
|
|||
* Voters should see results as soon as they have voted.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.disclosed")
|
||||
DISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.disclosed")
|
||||
DISCLOSED,
|
||||
|
||||
/**
|
||||
* Results should be only revealed when the poll is ended.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.undisclosed")
|
||||
UNDISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.undisclosed")
|
||||
UNDISCLOSED
|
||||
}
|
||||
|
|
|
@ -26,5 +26,6 @@ data class ReactionInfo(
|
|||
@Json(name = "key") val key: String,
|
||||
// always null for reaction
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
@Json(name = "option") override val option: Int? = null,
|
||||
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
|
||||
) : RelationContent
|
||||
|
|
|
@ -24,4 +24,10 @@ interface RelationContent {
|
|||
val eventId: String?
|
||||
val inReplyTo: ReplyToContent?
|
||||
val option: Int?
|
||||
|
||||
/**
|
||||
* This flag indicates that the message should be rendered as a reply
|
||||
* fallback, when isFallingBack = false
|
||||
*/
|
||||
val isFallingBack: Boolean?
|
||||
}
|
||||
|
|
|
@ -23,5 +23,8 @@ data class RelationDefaultContent(
|
|||
@Json(name = "rel_type") override val type: String?,
|
||||
@Json(name = "event_id") override val eventId: String?,
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
@Json(name = "option") override val option: Int? = null,
|
||||
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
|
||||
) : RelationContent
|
||||
|
||||
fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
|
||||
|
|
|
@ -163,13 +163,4 @@ interface RelationService {
|
|||
autoMarkdown: Boolean = false,
|
||||
formattedText: String? = null,
|
||||
eventReplied: TimelineEvent? = null): Cancelable?
|
||||
|
||||
/**
|
||||
* Get all the thread replies for the specified rootThreadEventId
|
||||
* The return list will contain the original root thread event and all the thread replies to that event
|
||||
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
|
||||
* from the backend
|
||||
* @param rootThreadEventId the root thread eventId
|
||||
*/
|
||||
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
|
||||
}
|
||||
|
|
|
@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ReplyToContent(
|
||||
@Json(name = "event_id") val eventId: String? = null,
|
||||
@Json(name = "render_in") val renderIn: List<String>? = null
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
||||
|
||||
fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
|
||||
|
|
|
@ -142,8 +142,9 @@ interface SendService {
|
|||
* @param latitude required latitude of the location
|
||||
* @param longitude required longitude of the location
|
||||
* @param uncertainty Accuracy of the location in meters
|
||||
* @param isUserLocation indicates whether the location data corresponds to the user location or not
|
||||
*/
|
||||
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
|
||||
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
|
||||
|
||||
/**
|
||||
* Remove this failed message from the timeline
|
||||
|
|
|
@ -33,8 +33,7 @@ object RoomSummaryConstants {
|
|||
EventType.ENCRYPTED,
|
||||
EventType.STICKER,
|
||||
EventType.REACTION,
|
||||
EventType.POLL_START
|
||||
)
|
||||
) + EventType.POLL_START
|
||||
|
||||
// SC addition | this is the Element behaviour previous to Element v1.0.7
|
||||
val PREVIEWABLE_TYPES_ALL = listOf(
|
||||
|
@ -54,7 +53,7 @@ object RoomSummaryConstants {
|
|||
EventType.STICKER,
|
||||
EventType.REACTION,
|
||||
EventType.STATE_ROOM_CREATE
|
||||
)
|
||||
) + EventType.POLL_START
|
||||
|
||||
// SC addition | no reactions in here
|
||||
val PREVIEWABLE_ORIGINAL_CONTENT_TYPES = listOf(
|
||||
|
@ -65,5 +64,5 @@ object RoomSummaryConstants {
|
|||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER
|
||||
)
|
||||
) + EventType.POLL_START
|
||||
}
|
||||
|
|
|
@ -17,51 +17,43 @@
|
|||
package org.matrix.android.sdk.api.session.room.threads
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with threads related features.
|
||||
* It's implemented at the room level within the main timeline.
|
||||
* This interface defines methods to interact with thread related features.
|
||||
* It's the dynamic threads implementation and the homeserver must return
|
||||
* a capability entry for threads. If the server do not support m.thread
|
||||
* then [ThreadsLocalService] should be used instead
|
||||
*/
|
||||
interface ThreadsService {
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
|
||||
* Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
|
||||
*/
|
||||
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||
fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the thread root TimelineEvents that exists at the room level
|
||||
* Returns a list of all the [ThreadSummary] that exists at the room level
|
||||
*/
|
||||
fun getAllThreads(): List<TimelineEvent>
|
||||
fun getAllThreadSummaries(): List<ThreadSummary>
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotifications(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns whether or not the current user is participating in the thread
|
||||
* @param rootThreadEventId the eventId of the current thread
|
||||
*/
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Enhance the provided root thread TimelineEvent [List] by adding the latest
|
||||
* Enhance the provided ThreadSummary[List] by adding the latest
|
||||
* message edition for that thread
|
||||
* @return the enhanced [List] with edited updates
|
||||
*/
|
||||
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
|
||||
fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
|
||||
|
||||
/**
|
||||
* Marks the current thread as read in local DB.
|
||||
* note: read receipts within threads are not yet supported with the API
|
||||
* @param rootThreadEventId the root eventId of the current thread
|
||||
* Fetch all thread replies for the specified thread using the /relations api
|
||||
* @param rootThreadEventId the root thread eventId
|
||||
* @param from defines the token that will fetch from that position
|
||||
* @param limit defines the number of max results the api will respond with
|
||||
*/
|
||||
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||
suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
|
||||
|
||||
/**
|
||||
* Fetch all thread summaries for the current room using the enhanced /messages api
|
||||
*/
|
||||
suspend fun fetchThreadSummaries()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.threads.local
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with thread related features.
|
||||
* It's the local threads implementation and assumes that the homeserver
|
||||
* do not support threads
|
||||
*/
|
||||
interface ThreadsLocalService {
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
|
||||
*/
|
||||
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the thread root TimelineEvents that exists at the room level
|
||||
*/
|
||||
fun getAllThreads(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotifications(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns whether or not the current user is participating in the thread
|
||||
* @param rootThreadEventId the eventId of the current thread
|
||||
*/
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Enhance the provided root thread TimelineEvent [List] by adding the latest
|
||||
* message edition for that thread
|
||||
* @return the enhanced [List] with edited updates
|
||||
*/
|
||||
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Marks the current thread as read in local DB.
|
||||
* note: read receipts within threads are not yet supported with the API
|
||||
* @param rootThreadEventId the root eventId of the current thread
|
||||
*/
|
||||
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.threads.model
|
||||
|
||||
data class ThreadEditions(var rootThreadEdition: String? = null,
|
||||
var latestThreadEdition: String? = null)
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.threads.model
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
|
||||
/**
|
||||
* The main thread Summary model, mainly used to display the thread list
|
||||
*/
|
||||
data class ThreadSummary(val roomId: String,
|
||||
val rootEvent: Event?,
|
||||
val latestEvent: Event?,
|
||||
val rootEventId: String,
|
||||
val rootThreadSenderInfo: SenderInfo,
|
||||
val latestThreadSenderInfo: SenderInfo,
|
||||
val isUserParticipating: Boolean,
|
||||
val numberOfThreads: Int,
|
||||
val threadEditions: ThreadEditions = ThreadEditions())
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.threads.model
|
||||
|
||||
enum class ThreadSummaryUpdateType {
|
||||
REPLACE,
|
||||
ADD
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.api.session.room.timeline
|
||||
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
|
@ -54,6 +55,7 @@ data class TimelineEvent(
|
|||
* It's not unique on the timeline as it's reset on each chunk.
|
||||
*/
|
||||
val displayIndex: Int,
|
||||
var ownedByThreadChunk: Boolean = false,
|
||||
val senderInfo: SenderInfo,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
|
@ -134,9 +136,9 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +160,13 @@ fun TimelineEvent.isSticker(): Boolean {
|
|||
return root.isSticker()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the event is a root thread event
|
||||
*/
|
||||
fun TimelineEvent.isRootThread(): Boolean {
|
||||
return root.threadDetails?.isRootThread.orFalse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
|
||||
*/
|
||||
|
|
|
@ -38,7 +38,7 @@ interface TimelineService {
|
|||
|
||||
/**
|
||||
* Returns a snapshot of TimelineEvent event with eventId.
|
||||
* At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
||||
* At the opposite of getTimelineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
||||
* @param eventId the eventId to get the TimelineEvent
|
||||
*/
|
||||
fun getTimelineEvent(eventId: String): TimelineEvent?
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.threads
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
|
||||
/**
|
||||
|
@ -26,7 +27,7 @@ data class ThreadDetails(
|
|||
val isRootThread: Boolean = false,
|
||||
val numberOfThreads: Int = 0,
|
||||
val threadSummarySenderInfo: SenderInfo? = null,
|
||||
val threadSummaryLatestTextMessage: String? = null,
|
||||
val threadSummaryLatestEvent: Event? = null,
|
||||
val lastMessageTimestamp: Long? = null,
|
||||
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
|
||||
val isThread: Boolean = false,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.api.util
|
||||
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
@ -199,6 +200,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
|
|||
|
||||
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
|
||||
|
||||
fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
|
||||
|
||||
fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
|
||||
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
|
||||
} else {
|
||||
|
|
|
@ -38,7 +38,7 @@ internal data class HomeServerVersion(
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
|
||||
internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
|
||||
|
||||
internal fun parse(value: String): HomeServerVersion? {
|
||||
val result = pattern.matchEntire(value) ?: return null
|
||||
|
@ -56,5 +56,6 @@ internal data class HomeServerVersion(
|
|||
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
|
||||
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
|
||||
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
|
||||
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
|
|||
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
|
||||
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
|
||||
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
|
||||
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
|
||||
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
|
||||
|
||||
/**
|
||||
* Return true if the SDK supports this homeserver version
|
||||
|
@ -68,6 +70,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
|
|||
doesServerSeparatesAddAndBind()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if the homeserver support MSC3440 for threads
|
||||
*/
|
||||
internal fun Versions.doesServerSupportThreads(): Boolean {
|
||||
return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
|
||||
unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the server support the lazy loading of room members
|
||||
*
|
||||
|
|
|
@ -434,6 +434,14 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
||||
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
||||
}
|
||||
|
||||
// unwedge if needed
|
||||
try {
|
||||
eventDecryptor.unwedgeDevicesIfNeeded()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
|
||||
}
|
||||
|
||||
// There is a limit of to_device events returned per sync.
|
||||
// If we are in a case of such limited to_device sync we can't try to generate/upload
|
||||
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
|
||||
|
@ -723,7 +731,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
|
@ -746,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return eventDecryptor.decryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
|
@ -1364,6 +1372,9 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
@VisibleForTesting
|
||||
val cryptoStoreForTesting = cryptoStore
|
||||
|
||||
@VisibleForTesting
|
||||
val olmDeviceForTest = olmDevice
|
||||
|
||||
companion object {
|
||||
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
|
||||
}
|
||||
|
|
|
@ -21,14 +21,13 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -40,6 +39,8 @@ import javax.inject.Inject
|
|||
|
||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
||||
|
||||
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class EventDecryptor @Inject constructor(
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
|
@ -47,13 +48,22 @@ internal class EventDecryptor @Inject constructor(
|
|||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore
|
||||
) {
|
||||
|
||||
// The date of the last time we forced establishment
|
||||
// of a new session for each user:device.
|
||||
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
||||
/**
|
||||
* Rate limit unwedge attempt, should we persist that?
|
||||
*/
|
||||
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
|
||||
|
||||
data class WedgedDeviceInfo(
|
||||
val userId: String,
|
||||
val senderKey: String?
|
||||
)
|
||||
|
||||
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
|
@ -63,7 +73,7 @@ internal class EventDecryptor @Inject constructor(
|
|||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
|
@ -91,38 +101,32 @@ internal class EventDecryptor @Inject constructor(
|
|||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
||||
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
||||
Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
try {
|
||||
return alg.decryptEvent(event, timeline)
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base &&
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
cryptoStore.getUserDevices(event.senderId ?: "")
|
||||
?.values
|
||||
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
||||
?.let {
|
||||
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
||||
}
|
||||
?: run {
|
||||
Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
|
||||
}
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,53 +136,91 @@ internal class EventDecryptor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// coroutineDispatchers.crypto scope
|
||||
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
val deviceKey = deviceInfo.identityKey()
|
||||
private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
|
||||
val info = WedgedDeviceInfo(senderId, senderKey)
|
||||
if (!wedgedDevices.contains(info)) {
|
||||
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
|
||||
wedgedDevices.add(info)
|
||||
}
|
||||
}
|
||||
|
||||
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||
// coroutineDispatchers.crypto scope
|
||||
suspend fun unwedgeDevicesIfNeeded() {
|
||||
// handle wedged devices
|
||||
// Some olm decryption have failed and some device are wedged
|
||||
// we should force start a new session for those
|
||||
Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
|
||||
// get the one that should be retried according to rate limit
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||
val toUnwedge = wedgedDevices.filter {
|
||||
val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
|
||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
|
||||
return@filter false
|
||||
}
|
||||
// let's already mark that we tried now
|
||||
lastNewSessionForcedDates[it] = now
|
||||
true
|
||||
}
|
||||
|
||||
if (toUnwedge.isEmpty()) {
|
||||
Timber.tag(loggerTag.value).v("Nothing to unwedge")
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
|
||||
|
||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||
|
||||
// offload this from crypto thread (?)
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
|
||||
onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
|
||||
onFailure = {
|
||||
Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
|
||||
toUnwedge
|
||||
.chunked(100) // safer to chunk if we ever have lots of wedged devices
|
||||
.forEach { wedgedList ->
|
||||
val groupedByUserId = wedgedList.groupBy { it.userId }
|
||||
// lets download keys if needed
|
||||
withContext(coroutineDispatchers.io) {
|
||||
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap<MXOlmSessionResult>, deviceInfo: CryptoDeviceInfo, senderId: String) {
|
||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
|
||||
// find the matching devices
|
||||
groupedByUserId
|
||||
.map { groupedByUser ->
|
||||
val userId = groupedByUser.key
|
||||
val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
|
||||
val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
||||
userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
|
||||
knownDevices.firstOrNull { it.identityKey() == senderKey }
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
.let { deviceList ->
|
||||
try {
|
||||
// force creating new outbound session and mark them as most recent to
|
||||
// be used for next encryption (dummy)
|
||||
val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
|
||||
Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
// We send this first such that, as long as the toDevice messages arrive in the
|
||||
// same order we sent them, the other end will get this first, set up the new session,
|
||||
// then get the keyshare request and send the key over this new session (because it
|
||||
// is the session it has most recently received a message on).
|
||||
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||
// Now send a dummy message on that session so the other side knows about it.
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.DUMMY
|
||||
)
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sessionToUse.map.values
|
||||
.flatMap { it.values }
|
||||
.map { it.deviceInfo }
|
||||
.forEach { deviceInfo ->
|
||||
Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
|
||||
}
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
|
||||
withContext(coroutineDispatchers.io) {
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
try {
|
||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
|
||||
}
|
||||
}
|
||||
// now let's send that
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
|
||||
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue