mirror of
https://github.com/element-hq/element-android
synced 2024-11-29 05:58:50 +03:00
Merge branch 'develop' into feature/ons/poll_timeline
* develop: (88 commits) Fix "PendingIntents attached to actions with remote inputs must be mutable" Room notifications are now working on Android 12 emulator Move P1 issues to app team and crypto boards Quick fix on this file Fix warning after rebase (it's for test, so OK to suppress warning) Update changelog, since the feature is not visible yet. Create file for Toast style (more generic) And improve fragment_create_poll.xml preview rendering Create CallToAction button style Create dedicated file for TextInputLayout styles And follow naming convention Lint fix. Add comment to run on Android 12 Use correct value, but I do not see any effect on emulator with API 12 Fix crash on Android 12. I guess we accept only images coming from the keyboard. ktlint Fix lint issue "NullSafeMutableLiveData" Fix lint issue "Incorrect constant" Fix lint issue "Outside Range" Ensure that column index is not -1 Make the Cursor extensions public And make the code more efficient, since we call getColumnIndexOrNull only once and not on each cursor iteration Fix crash on Android 12: PendingIntent.FLAG_IMMUTABLE has to be set Fix crash on Android 12 java.lang.SecurityException: To use the sampling rate of 0 microseconds, app needs to declare the normal permission HIGH_SAMPLING_RATE_SENSORS. InputConnectionCompat.createWrapper is deprecated Permission should be granted, according to https://developer.android.com/reference/android/view/OnReceiveContentListener#uri-permissions ...
This commit is contained in:
commit
eb1519743d
102 changed files with 1653 additions and 739 deletions
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -23,7 +23,7 @@ body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: result
|
id: result
|
||||||
attributes:
|
attributes:
|
||||||
label: Intended result and actual result
|
label: Outcome
|
||||||
placeholder: Tell us what went wrong
|
placeholder: Tell us what went wrong
|
||||||
value: |
|
value: |
|
||||||
#### What did you expect?
|
#### What did you expect?
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
|
@ -10,7 +10,7 @@ body:
|
||||||
id: usecase
|
id: usecase
|
||||||
attributes:
|
attributes:
|
||||||
label: Your use case
|
label: Your use case
|
||||||
description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
|
description: Please feel welcome to include screenshots or mock ups.
|
||||||
placeholder: Tell us what you would like to do!
|
placeholder: Tell us what you would like to do!
|
||||||
value: |
|
value: |
|
||||||
#### What would you like to do?
|
#### What would you like to do?
|
||||||
|
|
21
.github/workflows/sanity_test.yml
vendored
21
.github/workflows/sanity_test.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
api-level: [28]
|
api-level: [ 29 ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
@ -56,7 +56,24 @@ jobs:
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
- name: Run sanity tests on API ${{ matrix.api-level }}
|
- name: Run sanity tests on API ${{ matrix.api-level }}
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
continue-on-error: true # allow pipeline to upload failure results
|
||||||
with:
|
with:
|
||||||
|
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
api-level: ${{ matrix.api-level }}
|
api-level: ${{ matrix.api-level }}
|
||||||
script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest
|
emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||||
|
script: |
|
||||||
|
adb root
|
||||||
|
adb logcat -c
|
||||||
|
touch emulator.log
|
||||||
|
chmod 777 emulator.log
|
||||||
|
adb logcat >> emulator.log &
|
||||||
|
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
|
||||||
|
|
||||||
|
- name: Upload Failing Test Report Log
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: sanity-error-results
|
||||||
|
path: |
|
||||||
|
emulator.log
|
||||||
|
failure_screenshots/
|
||||||
|
|
2
.github/workflows/triage-incoming.yml
vendored
2
.github/workflows/triage-incoming.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
||||||
automate-project-columns:
|
automate-project-columns:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: alex-page/github-project-automation-plus@v0.8.1
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
with:
|
with:
|
||||||
project: Issue triage
|
project: Issue triage
|
||||||
column: Incoming
|
column: Incoming
|
||||||
|
|
101
.github/workflows/triage-move-labelled.yml
vendored
Normal file
101
.github/workflows/triage-move-labelled.yml
vendored
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
name: Move labelled issues to correct boards and columns
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
move_needs_info_issues:
|
||||||
|
name: Move X-Needs-Info issues to Need info on triage board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
||||||
|
with:
|
||||||
|
action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
|
||||||
|
project-url: "https://github.com/vector-im/element-android/projects/4"
|
||||||
|
column-name: "Need info"
|
||||||
|
label-name: "X-Needs-Info"
|
||||||
|
|
||||||
|
add_priority_design_issues_to_project:
|
||||||
|
name: Move priority X-Needs-Design issues to Design project board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Occasional')) &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'S-Critical') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Major') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Minor'))
|
||||||
|
steps:
|
||||||
|
- uses: octokit/graphql-action@v2.x
|
||||||
|
id: add_to_project
|
||||||
|
with:
|
||||||
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
|
query: |
|
||||||
|
mutation add_to_project($projectid:String!,$contentid:String!) {
|
||||||
|
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projectid: ${{ env.PROJECT_ID }}
|
||||||
|
contentid: ${{ github.event.issue.node_id }}
|
||||||
|
env:
|
||||||
|
PROJECT_ID: "PN_kwDOAM0swc0sUA"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
|
move_spaces_issues:
|
||||||
|
name: Move Spaces issues to Delight project board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Spaces') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Subspaces')
|
||||||
|
steps:
|
||||||
|
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
||||||
|
with:
|
||||||
|
action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
|
||||||
|
project-url: "https://github.com/orgs/vector-im/projects/6"
|
||||||
|
column-name: "📥 Inbox"
|
||||||
|
label-name: "A-Spaces"
|
||||||
|
- uses: octokit/graphql-action@v2.x
|
||||||
|
id: add_to_delight2
|
||||||
|
with:
|
||||||
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
|
query: |
|
||||||
|
mutation add_to_project($projectid:String!,$contentid:String!) {
|
||||||
|
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projectid: ${{ env.PROJECT_ID }}
|
||||||
|
contentid: ${{ github.event.issue.node_id }}
|
||||||
|
env:
|
||||||
|
PROJECT_ID: "PN_kwDOAM0swc1HvQ"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
|
move_voice-message_issues:
|
||||||
|
name: Move A-Voice Messages to Voice message board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
|
||||||
|
steps:
|
||||||
|
- uses: octokit/graphql-action@v2.x
|
||||||
|
with:
|
||||||
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
|
query: |
|
||||||
|
mutation add_to_project($projectid:String!,$contentid:String!) {
|
||||||
|
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projectid: ${{ env.PROJECT_ID }}
|
||||||
|
contentid: ${{ github.event.issue.node_id }}
|
||||||
|
env:
|
||||||
|
PROJECT_ID: "PN_kwDOAM0swc2KCw"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
35
.github/workflows/triage-move-unlabelled.yml
vendored
Normal file
35
.github/workflows/triage-move-unlabelled.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: Move unlabelled from needs info columns to triaged
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [unlabeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Move_Unabeled_Issue_On_Project_Board:
|
||||||
|
name: Move no longer X-Needs-Info issues to Triaged
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
${{
|
||||||
|
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
|
||||||
|
env:
|
||||||
|
BOARD_NAME: "Issue triage"
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
REPO: ${{ github.event.repository.name }}
|
||||||
|
ISSUE: ${{ github.event.issue.number }}
|
||||||
|
steps:
|
||||||
|
- name: Check if issue is already in "${{ env.BOARD_NAME }}"
|
||||||
|
run: |
|
||||||
|
if curl -i -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql | grep "\b$BOARD_NAME\b"; then
|
||||||
|
echo "Issue is already in Project '$BOARD_NAME', proceeding";
|
||||||
|
echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
|
||||||
|
echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
- name: Move issue
|
||||||
|
uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
|
if: ${{ env.ALREADY_IN_BOARD == 'true' }}
|
||||||
|
with:
|
||||||
|
project: Issue triage
|
||||||
|
column: Triaged
|
||||||
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
16
.github/workflows/triage-needs-info.yml
vendored
16
.github/workflows/triage-needs-info.yml
vendored
|
@ -1,16 +0,0 @@
|
||||||
name: Move X-Needs-Info into Need info column in the Issue triage board
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Move_Labeled_Issue_On_Project_Board:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: konradpabjan/move-labeled-or-milestoned-issue@v2.0
|
|
||||||
with:
|
|
||||||
action-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
project-url: "https://github.com/vector-im/element-android/projects/4"
|
|
||||||
column-name: "Need info"
|
|
||||||
label-name: "X-Needs-Info"
|
|
55
.github/workflows/triage-priority-bugs.yml
vendored
Normal file
55
.github/workflows/triage-priority-bugs.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: Move P1 issues into the P1 column for the App Team and Crypto team
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
p1_issues_to_team_workboard:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-Spaces') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||||
|
steps:
|
||||||
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
|
with:
|
||||||
|
project: Android App Team
|
||||||
|
column: P1
|
||||||
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
|
P1_issues_to_crypto_team_workboard:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
||||||
|
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
|
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||||
|
steps:
|
||||||
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
|
with:
|
||||||
|
project: Crypto Team
|
||||||
|
column: Ready
|
||||||
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package im.vector.lib.attachmentviewer
|
package im.vector.lib.attachmentviewer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -141,7 +142,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
} else {
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
|
@ -347,7 +353,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
||||||
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
} else {
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
|
|
1
changelog.d/4257.misc
Normal file
1
changelog.d/4257.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Remove redundant text in feature request issue form
|
|
@ -1 +1 @@
|
||||||
Poll Feature - Create Poll Screen
|
Poll Feature - Create Poll Screen (Disabled for now)
|
1
changelog.d/4401.removal
Normal file
1
changelog.d/4401.removal
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Breaking SDK API change to PushRuleListener, the separated callbacks have been merged into one with a data class which includes all the previously separated push information
|
1
changelog.d/4402.feature
Normal file
1
changelog.d/4402.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Adds support for images inside message notifications
|
1
changelog.d/4424.bugfix
Normal file
1
changelog.d/4424.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix incorrect cropping of conversation icons
|
1
changelog.d/4435.misc
Normal file
1
changelog.d/4435.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add and improve issue triage workflows
|
1
changelog.d/4446.bugfix
Normal file
1
changelog.d/4446.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Unable to establish Olm outbound session from fallback key
|
1
changelog.d/4452.misc
Normal file
1
changelog.d/4452.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Update issue template to bring in line with element-web
|
|
@ -1,8 +1,8 @@
|
||||||
ext.versions = [
|
ext.versions = [
|
||||||
|
|
||||||
'minSdk' : 21,
|
'minSdk' : 21,
|
||||||
'compileSdk' : 30,
|
'compileSdk' : 31,
|
||||||
'targetSdk' : 30,
|
'targetSdk' : 31,
|
||||||
'sourceCompat' : JavaVersion.VERSION_11,
|
'sourceCompat' : JavaVersion.VERSION_11,
|
||||||
'targetCompat' : JavaVersion.VERSION_11,
|
'targetCompat' : JavaVersion.VERSION_11,
|
||||||
]
|
]
|
||||||
|
@ -11,12 +11,12 @@ def gradle = "7.0.3"
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
def kotlin = "1.5.31"
|
def kotlin = "1.5.31"
|
||||||
def kotlinCoroutines = "1.5.2"
|
def kotlinCoroutines = "1.5.2"
|
||||||
def dagger = "2.40"
|
def dagger = "2.40.1"
|
||||||
def retrofit = "2.9.0"
|
def retrofit = "2.9.0"
|
||||||
def arrow = "0.8.2"
|
def arrow = "0.8.2"
|
||||||
def markwon = "4.6.2"
|
def markwon = "4.6.2"
|
||||||
def moshi = "1.12.0"
|
def moshi = "1.12.0"
|
||||||
def lifecycle = "2.2.0"
|
def lifecycle = "2.4.0"
|
||||||
def flowBinding = "1.2.0"
|
def flowBinding = "1.2.0"
|
||||||
def epoxy = "4.6.2"
|
def epoxy = "4.6.2"
|
||||||
def mavericks = "2.4.0"
|
def mavericks = "2.4.0"
|
||||||
|
@ -46,18 +46,18 @@ ext.libs = [
|
||||||
],
|
],
|
||||||
androidx : [
|
androidx : [
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.3.1",
|
'appCompat' : "androidx.appcompat:appcompat:1.3.1",
|
||||||
'core' : "androidx.core:core-ktx:1.6.0",
|
'core' : "androidx.core:core-ktx:1.7.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
||||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6",
|
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6",
|
||||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1",
|
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1",
|
||||||
'work' : "androidx.work:work-runtime-ktx:2.6.0",
|
'work' : "androidx.work:work-runtime-ktx:2.7.0",
|
||||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
||||||
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
|
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
|
||||||
'junit' : "androidx.test.ext:junit:1.1.3",
|
'junit' : "androidx.test.ext:junit:1.1.3",
|
||||||
'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle",
|
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||||
'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle",
|
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1",
|
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
|
distributionSha256Sum=00b273629df4ce46e68df232161d5a7c4e495b9a029ce6e0420f071e21316867
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -129,9 +129,9 @@
|
||||||
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
|
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
|
||||||
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
|
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
|
||||||
|
|
||||||
<attr name="vctr_voice_message_toast_background" format="color" />
|
<attr name="vctr_toast_background" format="color" />
|
||||||
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color>
|
<color name="vctr_toast_background_light">@color/palette_black_900</color>
|
||||||
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color>
|
<color name="vctr_toast_background_dark">@color/palette_gray_400</color>
|
||||||
|
|
||||||
<!-- Presence Indicator colors -->
|
<!-- Presence Indicator colors -->
|
||||||
<attr name="vctr_presence_indicator_offline" format="color" />
|
<attr name="vctr_presence_indicator_offline" format="color" />
|
||||||
|
|
|
@ -10,6 +10,11 @@
|
||||||
<item name="lineHeight">24sp</item>
|
<item name="lineHeight">24sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Vector.Button.CallToAction" parent="Widget.Vector.Button">
|
||||||
|
<item name="android:backgroundTint">@color/button_background_tint_selector</item>
|
||||||
|
<item name="android:textColor">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Destructive">
|
<style name="Widget.Vector.Button.Destructive">
|
||||||
<item name="android:minWidth">94dp</item>
|
<item name="android:minWidth">94dp</item>
|
||||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
|
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Default style for TextInputLayout -->
|
|
||||||
<style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
|
|
||||||
|
|
||||||
<style name="Widget.Vector.TextInputLayout.Password">
|
|
||||||
<item name="endIconMode">password_toggle</item>
|
|
||||||
<item name="endIconTint">?vctr_content_secondary</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText">
|
<style name="Widget.Vector.EditText.Composer" parent="Widget.AppCompat.EditText">
|
||||||
<item name="android:background">@android:color/transparent</item>
|
<item name="android:background">@android:color/transparent</item>
|
||||||
<item name="android:inputType">textCapSentences|textMultiLine</item>
|
<item name="android:inputType">textCapSentences|textMultiLine</item>
|
||||||
|
@ -19,9 +11,4 @@
|
||||||
<item name="android:textColor">?vctr_message_text_color</item>
|
<item name="android:textColor">?vctr_message_text_color</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.EditText.Form" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
|
||||||
<item name="boxStrokeColor">@color/form_edit_text_stroke_color_selector</item>
|
|
||||||
<item name="android:textColorHint">@color/form_edit_text_hint_color_selector</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.CreatePoll" parent="Widget.Vector.Button">
|
|
||||||
<item name="android:backgroundTint">@color/button_background_tint_selector</item>
|
|
||||||
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
|
|
||||||
<item name="android:textColor">@android:color/white</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Default style for TextInputLayout -->
|
||||||
|
<style name="Widget.Vector.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
|
||||||
|
|
||||||
|
<style name="Widget.Vector.TextInputLayout.Password">
|
||||||
|
<item name="endIconMode">password_toggle</item>
|
||||||
|
<item name="endIconTint">?vctr_content_secondary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Vector.TextInputLayout.Form">
|
||||||
|
<item name="boxStrokeColor">@color/form_edit_text_stroke_color_selector</item>
|
||||||
|
<item name="android:textColorHint">@color/form_edit_text_hint_color_selector</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
15
library/ui-styles/src/main/res/values/styles_toast.xml
Normal file
15
library/ui-styles/src/main/res/values/styles_toast.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Widget.Vector.TextView.Caption.Toast">
|
||||||
|
<item name="android:paddingTop">8dp</item>
|
||||||
|
<item name="android:paddingBottom">8dp</item>
|
||||||
|
<item name="android:paddingStart">12dp</item>
|
||||||
|
<item name="android:paddingEnd">12dp</item>
|
||||||
|
<item name="android:background">@drawable/bg_round_corner_8dp</item>
|
||||||
|
<item name="android:backgroundTint">?vctr_toast_background</item>
|
||||||
|
<item name="android:textColor">@color/palette_white</item>
|
||||||
|
<item name="android:gravity">center</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -12,15 +12,4 @@
|
||||||
<item name="direction">rightToLeft</item>
|
<item name="direction">rightToLeft</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.TextView.Caption.Toast">
|
|
||||||
<item name="android:paddingTop">8dp</item>
|
|
||||||
<item name="android:paddingBottom">8dp</item>
|
|
||||||
<item name="android:paddingStart">12dp</item>
|
|
||||||
<item name="android:paddingEnd">12dp</item>
|
|
||||||
<item name="android:background">@drawable/bg_round_corner_8dp</item>
|
|
||||||
<item name="android:backgroundTint">?vctr_voice_message_toast_background</item>
|
|
||||||
<item name="android:textColor">@color/palette_white</item>
|
|
||||||
<item name="android:gravity">center</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -140,8 +140,7 @@
|
||||||
<!-- Keywords -->
|
<!-- Keywords -->
|
||||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||||
|
|
||||||
<!-- Voice Message -->
|
<item name="vctr_toast_background">@color/vctr_toast_background_dark</item>
|
||||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||||
|
|
|
@ -143,8 +143,7 @@
|
||||||
<!-- Keywords -->
|
<!-- Keywords -->
|
||||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||||
|
|
||||||
<!-- Voice Message -->
|
<item name="vctr_toast_background">@color/vctr_toast_background_light</item>
|
||||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||||
|
|
|
@ -44,6 +44,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
|
// Comment to run on Android 12
|
||||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,8 +107,9 @@ dependencies {
|
||||||
implementation libs.androidx.appCompat
|
implementation libs.androidx.appCompat
|
||||||
implementation libs.androidx.core
|
implementation libs.androidx.core
|
||||||
|
|
||||||
implementation libs.androidx.lifecycleExtensions
|
// Lifecycle
|
||||||
implementation libs.androidx.lifecycleJava8
|
implementation libs.androidx.lifecycleCommon
|
||||||
|
implementation libs.androidx.lifecycleProcess
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
implementation libs.squareup.retrofit
|
implementation libs.squareup.retrofit
|
||||||
|
@ -156,10 +158,10 @@ dependencies {
|
||||||
implementation libs.apache.commonsImaging
|
implementation libs.apache.commonsImaging
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
|
||||||
|
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
testImplementation 'org.robolectric:robolectric:4.7'
|
||||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||||
testImplementation libs.mockk.mockk
|
testImplementation libs.mockk.mockk
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.matrix.android.sdk.api.pushrules
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
|
||||||
|
data class PushEvents(
|
||||||
|
val matchedEvents: List<Pair<Event, PushRule>>,
|
||||||
|
val roomsJoined: Collection<String>,
|
||||||
|
val roomsLeft: Collection<String>,
|
||||||
|
val redactedEventIds: List<String>
|
||||||
|
)
|
|
@ -51,11 +51,7 @@ interface PushRuleService {
|
||||||
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
||||||
|
|
||||||
interface PushRuleListener {
|
interface PushRuleListener {
|
||||||
fun onMatchRule(event: Event, actions: List<Action>)
|
fun onEvents(pushEvents: PushEvents)
|
||||||
fun onRoomJoined(roomId: String)
|
|
||||||
fun onRoomLeft(roomId: String)
|
|
||||||
fun onEventRedacted(redactedEventId: String)
|
|
||||||
fun batchFinish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeywords(): LiveData<Set<String>>
|
fun getKeywords(): LiveData<Set<String>>
|
||||||
|
|
|
@ -38,14 +38,22 @@ data class MXKey(
|
||||||
/**
|
/**
|
||||||
* signature user Id to [deviceid][signature]
|
* signature user Id to [deviceid][signature]
|
||||||
*/
|
*/
|
||||||
private val signatures: Map<String, Map<String, String>>
|
private val signatures: Map<String, Map<String, String>>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to store the original json because it can contain other fields
|
||||||
|
* that we don't support yet but they would be needed to check signatures
|
||||||
|
*/
|
||||||
|
private val rawMap: JsonDict
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the signed data map
|
* @return the signed data map
|
||||||
*/
|
*/
|
||||||
fun signalableJSONDictionary(): Map<String, Any> {
|
fun signalableJSONDictionary(): Map<String, Any> {
|
||||||
return mapOf("key" to value)
|
return rawMap.filter {
|
||||||
|
it.key != "signatures" && it.key != "unsigned"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,6 +90,7 @@ data class MXKey(
|
||||||
* <pre>
|
* <pre>
|
||||||
* "signed_curve25519:AAAAFw": {
|
* "signed_curve25519:AAAAFw": {
|
||||||
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
|
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
|
||||||
|
* "fallback" : true|false
|
||||||
* "signatures": {
|
* "signatures": {
|
||||||
* "@userId:matrix.org": {
|
* "@userId:matrix.org": {
|
||||||
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
|
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
|
||||||
|
@ -107,7 +116,8 @@ data class MXKey(
|
||||||
type = components[0],
|
type = components[0],
|
||||||
keyId = components[1],
|
keyId = components[1],
|
||||||
value = params["key"] as String,
|
value = params["key"] as String,
|
||||||
signatures = params["signatures"] as Map<String, Map<String, String>>
|
signatures = params["signatures"] as Map<String, Map<String, String>>,
|
||||||
|
rawMap = params
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.pushrules.Action
|
import org.matrix.android.sdk.api.pushrules.Action
|
||||||
|
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||||
|
@ -142,79 +143,6 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||||
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun processEvents(events: List<Event>) {
|
|
||||||
// var hasDoneSomething = false
|
|
||||||
// events.forEach { event ->
|
|
||||||
// fulfilledBingRule(event)?.let {
|
|
||||||
// hasDoneSomething = true
|
|
||||||
// dispatchBing(event, it)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (hasDoneSomething)
|
|
||||||
// dispatchFinish()
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun dispatchBing(event: Event, rule: PushRule) {
|
|
||||||
synchronized(listeners) {
|
|
||||||
val actionsList = rule.getActions()
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onMatchRule(event, actionsList)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Error while dispatching bing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchRoomJoined(roomId: String) {
|
|
||||||
synchronized(listeners) {
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onRoomJoined(roomId)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Error while dispatching room joined")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchRoomLeft(roomId: String) {
|
|
||||||
synchronized(listeners) {
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onRoomLeft(roomId)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Error while dispatching room left")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchRedactedEventId(redactedEventId: String) {
|
|
||||||
synchronized(listeners) {
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onEventRedacted(redactedEventId)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Error while dispatching redacted event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchFinish() {
|
|
||||||
synchronized(listeners) {
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.batchFinish()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "Error while dispatching finish")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getKeywords(): LiveData<Set<String>> {
|
override fun getKeywords(): LiveData<Set<String>> {
|
||||||
// Keywords are all content rules that don't start with '.'
|
// Keywords are all content rules that don't start with '.'
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
@ -229,4 +157,16 @@ internal class DefaultPushRuleService @Inject constructor(
|
||||||
results.firstOrNull().orEmpty().toSet()
|
results.firstOrNull().orEmpty().toSet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dispatchEvents(pushEvents: PushEvents) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.forEach {
|
||||||
|
try {
|
||||||
|
it.onEvents(pushEvents)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.e(e, "Error while dispatching push events")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.notification
|
package org.matrix.android.sdk.internal.session.notification
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.isInvitation
|
import org.matrix.android.sdk.api.session.events.model.isInvitation
|
||||||
|
@ -39,14 +40,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
) : ProcessEventForPushTask {
|
) : ProcessEventForPushTask {
|
||||||
|
|
||||||
override suspend fun execute(params: ProcessEventForPushTask.Params) {
|
override suspend fun execute(params: ProcessEventForPushTask.Params) {
|
||||||
// Handle left rooms
|
|
||||||
params.syncResponse.leave.keys.forEach {
|
|
||||||
defaultPushRuleService.dispatchRoomLeft(it)
|
|
||||||
}
|
|
||||||
// Handle joined rooms
|
|
||||||
params.syncResponse.join.keys.forEach {
|
|
||||||
defaultPushRuleService.dispatchRoomJoined(it)
|
|
||||||
}
|
|
||||||
val newJoinEvents = params.syncResponse.join
|
val newJoinEvents = params.syncResponse.join
|
||||||
.mapNotNull { (key, value) ->
|
.mapNotNull { (key, value) ->
|
||||||
value.timeline?.events?.mapNotNull {
|
value.timeline?.events?.mapNotNull {
|
||||||
|
@ -74,10 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
}
|
}
|
||||||
Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
|
Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
|
||||||
" to check for push rules with ${params.rules.size} rules")
|
" to check for push rules with ${params.rules.size} rules")
|
||||||
allEvents.forEach { event ->
|
val matchedEvents = allEvents.mapNotNull { event ->
|
||||||
pushRuleFinder.fulfilledBingRule(event, params.rules)?.let {
|
pushRuleFinder.fulfilledBingRule(event, params.rules)?.let {
|
||||||
Timber.v("[PushRules] Rule $it match for event ${event.eventId}")
|
Timber.v("[PushRules] Rule $it match for event ${event.eventId}")
|
||||||
defaultPushRuleService.dispatchBing(event, it)
|
event to it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +84,13 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
|
|
||||||
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
|
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
|
||||||
|
|
||||||
allRedactedEvents.forEach { redactedEventId ->
|
defaultPushRuleService.dispatchEvents(
|
||||||
defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
|
PushEvents(
|
||||||
}
|
matchedEvents = matchedEvents,
|
||||||
|
roomsJoined = params.syncResponse.join.keys,
|
||||||
defaultPushRuleService.dispatchFinish()
|
roomsLeft = params.syncResponse.leave.keys,
|
||||||
|
redactedEventIds = allRedactedEvents
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.util
|
package org.matrix.android.sdk.internal.util
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import org.matrix.android.sdk.internal.di.MatrixScope
|
import org.matrix.android.sdk.internal.di.MatrixScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -27,13 +26,12 @@ import javax.inject.Inject
|
||||||
* To be attached to ProcessLifecycleOwner lifecycle
|
* To be attached to ProcessLifecycleOwner lifecycle
|
||||||
*/
|
*/
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
|
internal class BackgroundDetectionObserver @Inject constructor() : DefaultLifecycleObserver {
|
||||||
|
|
||||||
var isInBackground: Boolean = true
|
var isInBackground: Boolean = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private
|
private val listeners = LinkedHashSet<Listener>()
|
||||||
val listeners = LinkedHashSet<Listener>()
|
|
||||||
|
|
||||||
fun register(listener: Listener) {
|
fun register(listener: Listener) {
|
||||||
listeners.add(listener)
|
listeners.add(listener)
|
||||||
|
@ -43,15 +41,13 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
|
||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
fun onMoveToForeground() {
|
|
||||||
Timber.v("App returning to foreground…")
|
Timber.v("App returning to foreground…")
|
||||||
isInBackground = false
|
isInBackground = false
|
||||||
listeners.forEach { it.onMoveToForeground() }
|
listeners.forEach { it.onMoveToForeground() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
fun onMoveToBackground() {
|
|
||||||
Timber.v("App going to background…")
|
Timber.v("App going to background…")
|
||||||
isInBackground = true
|
isInBackground = true
|
||||||
listeners.forEach { it.onMoveToBackground() }
|
listeners.forEach { it.onMoveToBackground() }
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
||||||
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact Picker implementation
|
* Contact Picker implementation
|
||||||
|
@ -49,9 +50,9 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
|
val idColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts._ID) ?: return@use
|
||||||
val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
|
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||||
val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI)
|
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
|
||||||
|
|
||||||
val contactId = cursor.getInt(idColumn)
|
val contactId = cursor.getInt(idColumn)
|
||||||
var name = cursor.getString(nameColumn)
|
var name = cursor.getString(nameColumn)
|
||||||
|
@ -72,10 +73,13 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
selection,
|
selection,
|
||||||
selectionArgs,
|
selectionArgs,
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use inner@{ innerCursor ->
|
||||||
while (cursor.moveToNext()) {
|
val mimeTypeColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.MIMETYPE) ?: return@inner
|
||||||
val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
|
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
|
||||||
val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
|
|
||||||
|
while (innerCursor.moveToNext()) {
|
||||||
|
val mimeType = innerCursor.getString(mimeTypeColumnIndex)
|
||||||
|
val contactData = innerCursor.getString(data1ColumnIndex)
|
||||||
|
|
||||||
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||||
name = contactData
|
name = contactData
|
||||||
|
@ -115,7 +119,10 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
selectionArgs,
|
selectionArgs,
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null
|
return if (cursor.moveToFirst()) {
|
||||||
|
cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
|
||||||
|
?.let { cursor.getInt(it) }
|
||||||
|
} else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Intent
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
||||||
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
import im.vector.lib.multipicker.utils.isMimeTypeAudio
|
import im.vector.lib.multipicker.utils.isMimeTypeAudio
|
||||||
import im.vector.lib.multipicker.utils.isMimeTypeImage
|
import im.vector.lib.multipicker.utils.isMimeTypeImage
|
||||||
import im.vector.lib.multipicker.utils.isMimeTypeVideo
|
import im.vector.lib.multipicker.utils.isMimeTypeVideo
|
||||||
|
@ -49,8 +50,8 @@ class FilePicker : Picker<MultiPickerBaseType>() {
|
||||||
// Other files
|
// Other files
|
||||||
context.contentResolver.query(selectedUri, null, null, null, null)
|
context.contentResolver.query(selectedUri, null, null, null, null)
|
||||||
?.use { cursor ->
|
?.use { cursor ->
|
||||||
val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
|
||||||
val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
|
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getString(nameColumn)
|
||||||
val size = cursor.getLong(sizeColumn)
|
val size = cursor.getLong(sizeColumn)
|
||||||
|
|
|
@ -37,8 +37,8 @@ internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType?
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.DISPLAY_NAME) ?: return@use null
|
||||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getString(nameColumn)
|
||||||
|
@ -75,8 +75,8 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
|
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.DISPLAY_NAME) ?: return@use null
|
||||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE)
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getString(nameColumn)
|
||||||
|
@ -124,8 +124,8 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME)
|
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.DISPLAY_NAME) ?: return@use null
|
||||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getString(nameColumn)
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.lib.multipicker.utils
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
|
||||||
|
fun Cursor.getColumnIndexOrNull(column: String): Int? {
|
||||||
|
return getColumnIndex(column).takeIf { it != -1 }
|
||||||
|
}
|
|
@ -4191,7 +4191,8 @@
|
||||||
"call",
|
"call",
|
||||||
"hand",
|
"hand",
|
||||||
"hands",
|
"hands",
|
||||||
"gesture"
|
"gesture",
|
||||||
|
"shaka"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"backhand-index-pointing-left": {
|
"backhand-index-pointing-left": {
|
||||||
|
|
|
@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1
|
||||||
PARAM_APK=$2
|
PARAM_APK=$2
|
||||||
|
|
||||||
# Other params
|
# Other params
|
||||||
BUILD_TOOLS_VERSION="30.0.3"
|
BUILD_TOOLS_VERSION="31.0.0-rc5"
|
||||||
MIN_SDK_VERSION=21
|
MIN_SDK_VERSION=21
|
||||||
|
|
||||||
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
||||||
|
|
|
@ -23,7 +23,7 @@ PARAM_KS_PASS=$3
|
||||||
PARAM_KEY_PASS=$4
|
PARAM_KEY_PASS=$4
|
||||||
|
|
||||||
# Other params
|
# Other params
|
||||||
BUILD_TOOLS_VERSION="30.0.3"
|
BUILD_TOOLS_VERSION="31.0.0-rc5"
|
||||||
MIN_SDK_VERSION=21
|
MIN_SDK_VERSION=21
|
||||||
|
|
||||||
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
||||||
|
|
|
@ -210,6 +210,7 @@ android {
|
||||||
// This property does not affect tests that you run using Android Studio.”
|
// This property does not affect tests that you run using Android Studio.”
|
||||||
animationsDisabled = true
|
animationsDisabled = true
|
||||||
|
|
||||||
|
// Comment to run on Android 12
|
||||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,8 +357,10 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.squareup.moshi
|
implementation libs.squareup.moshi
|
||||||
kapt libs.squareup.moshiKotlin
|
kapt libs.squareup.moshiKotlin
|
||||||
implementation libs.androidx.lifecycleExtensions
|
|
||||||
|
// Lifecycle
|
||||||
implementation libs.androidx.lifecycleLivedata
|
implementation libs.androidx.lifecycleLivedata
|
||||||
|
implementation libs.androidx.lifecycleProcess
|
||||||
|
|
||||||
implementation libs.androidx.datastore
|
implementation libs.androidx.datastore
|
||||||
implementation libs.androidx.datastorepreferences
|
implementation libs.androidx.datastorepreferences
|
||||||
|
@ -370,7 +373,7 @@ dependencies {
|
||||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
implementation libs.github.flowBinding
|
implementation libs.github.flowBinding
|
||||||
|
@ -411,7 +414,7 @@ dependencies {
|
||||||
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
||||||
|
|
||||||
// Custom Tab
|
// Custom Tab
|
||||||
implementation 'androidx.browser:browser:1.3.0'
|
implementation 'androidx.browser:browser:1.4.0'
|
||||||
|
|
||||||
// Passphrase strength helper
|
// Passphrase strength helper
|
||||||
implementation 'com.nulab-inc:zxcvbn:1.5.2'
|
implementation 'com.nulab-inc:zxcvbn:1.5.2'
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
import androidx.test.espresso.IdlingRegistry
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
@ -35,6 +36,11 @@ import androidx.test.runner.lifecycle.ActivityLifecycleCallback
|
||||||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
|
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
|
||||||
import androidx.test.runner.lifecycle.Stage
|
import androidx.test.runner.lifecycle.Stage
|
||||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.Matchers
|
import org.hamcrest.Matchers
|
||||||
import org.hamcrest.StringDescription
|
import org.hamcrest.StringDescription
|
||||||
|
@ -52,6 +58,18 @@ object EspressoHelper {
|
||||||
}
|
}
|
||||||
return currentActivity
|
return currentActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> getBottomSheetDialog(): BottomSheetDialogFragment? {
|
||||||
|
return (getCurrentActivity() as? FragmentActivity)
|
||||||
|
?.supportFragmentManager
|
||||||
|
?.fragments
|
||||||
|
?.filterIsInstance<T>()
|
||||||
|
?.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getString(@StringRes id: Int): String {
|
||||||
|
return EspressoHelper.getCurrentActivity()!!.resources.getString(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
|
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
|
||||||
|
@ -216,3 +234,46 @@ fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) {
|
||||||
block()
|
block()
|
||||||
Espresso.pressBack()
|
Espresso.pressBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> interactWithSheet(contentMatcher: Matcher<View>, noinline block: () -> Unit = {}) {
|
||||||
|
waitUntilViewVisible(contentMatcher)
|
||||||
|
val behaviour = (EspressoHelper.getBottomSheetDialog<T>()!!.dialog as BottomSheetDialog).behavior
|
||||||
|
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_EXPANDED), block)
|
||||||
|
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_HIDDEN)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSheetResource(
|
||||||
|
private val bottomSheetBehavior: BottomSheetBehavior<*>,
|
||||||
|
@BottomSheetBehavior.State private val wantedState: Int
|
||||||
|
) : IdlingResource, BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
|
||||||
|
private var isIdle: Boolean = false
|
||||||
|
private var resourceCallback: IdlingResource.ResourceCallback? = null
|
||||||
|
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||||
|
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
val wasIdle = isIdle
|
||||||
|
isIdle = newState == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
if (!wasIdle && isIdle) {
|
||||||
|
bottomSheetBehavior.removeBottomSheetCallback(this)
|
||||||
|
resourceCallback?.onTransitionToIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName() = "BottomSheet awaiting state: $wantedState"
|
||||||
|
|
||||||
|
override fun isIdleNow() = isIdle
|
||||||
|
|
||||||
|
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
|
||||||
|
resourceCallback = callback
|
||||||
|
|
||||||
|
val state = bottomSheetBehavior.state
|
||||||
|
isIdle = state == wantedState
|
||||||
|
if (isIdle) {
|
||||||
|
resourceCallback!!.onTransitionToIdle()
|
||||||
|
} else {
|
||||||
|
bottomSheetBehavior.addBottomSheetCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.espresso.tools
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
private val SCREENSHOT_FOLDER_LOCATION = "${Environment.DIRECTORY_PICTURES}/failure_screenshots"
|
||||||
|
private val deviceLanguage = Locale.getDefault().language
|
||||||
|
|
||||||
|
class ScreenshotFailureRule : TestWatcher() {
|
||||||
|
override fun failed(e: Throwable?, description: Description) {
|
||||||
|
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
|
||||||
|
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
|
||||||
|
storeFailureScreenshot(bitmap, screenShotName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores screenshots in sdcard/Pictures/failure_screenshots
|
||||||
|
*/
|
||||||
|
private fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) {
|
||||||
|
val contentResolver = getInstrumentation().targetContext.applicationContext.contentResolver
|
||||||
|
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||||
|
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
useMediaStoreScreenshotStorage(
|
||||||
|
contentValues,
|
||||||
|
contentResolver,
|
||||||
|
screenshotName,
|
||||||
|
SCREENSHOT_FOLDER_LOCATION,
|
||||||
|
bitmap
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
usePublicExternalScreenshotStorage(
|
||||||
|
contentValues,
|
||||||
|
contentResolver,
|
||||||
|
screenshotName,
|
||||||
|
SCREENSHOT_FOLDER_LOCATION,
|
||||||
|
bitmap
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun useMediaStoreScreenshotStorage(
|
||||||
|
contentValues: ContentValues,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
screenshotName: String,
|
||||||
|
screenshotLocation: String,
|
||||||
|
bitmap: Bitmap
|
||||||
|
) {
|
||||||
|
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "$screenshotName.jpeg")
|
||||||
|
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, screenshotLocation)
|
||||||
|
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
if (uri != null) {
|
||||||
|
contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) }
|
||||||
|
contentResolver.update(uri, contentValues, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun usePublicExternalScreenshotStorage(
|
||||||
|
contentValues: ContentValues,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
screenshotName: String,
|
||||||
|
screenshotLocation: String,
|
||||||
|
bitmap: Bitmap
|
||||||
|
) {
|
||||||
|
val directory = File(Environment.getExternalStoragePublicDirectory(screenshotLocation).toString())
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
val file = File(directory, "$screenshotName.jpeg")
|
||||||
|
saveScreenshotToStream(bitmap, FileOutputStream(file))
|
||||||
|
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) {
|
||||||
|
outputStream.use {
|
||||||
|
try {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, it)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Screenshot was not stored at this time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,10 +19,15 @@ package im.vector.app.ui
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.espresso.tools.ScreenshotFailureRule
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.getString
|
||||||
import im.vector.app.ui.robot.ElementRobot
|
import im.vector.app.ui.robot.ElementRobot
|
||||||
|
import im.vector.app.ui.robot.withDeveloperMode
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.rules.RuleChain
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -34,7 +39,9 @@ import java.util.UUID
|
||||||
class UiAllScreensSanityTest {
|
class UiAllScreensSanityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
val testRule = RuleChain
|
||||||
|
.outerRule(ActivityScenarioRule(MainActivity::class.java))
|
||||||
|
.around(ScreenshotFailureRule())
|
||||||
|
|
||||||
private val elementRobot = ElementRobot()
|
private val elementRobot = ElementRobot()
|
||||||
|
|
||||||
|
@ -69,13 +76,30 @@ class UiAllScreensSanityTest {
|
||||||
createNewRoom {
|
createNewRoom {
|
||||||
crawl()
|
crawl()
|
||||||
createRoom {
|
createRoom {
|
||||||
postMessage("Hello world!")
|
val message = "Hello world!"
|
||||||
|
postMessage(message)
|
||||||
crawl()
|
crawl()
|
||||||
|
crawlMessage(message)
|
||||||
openSettings { crawl() }
|
openSettings { crawl() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elementRobot.withDeveloperMode {
|
||||||
|
settings {
|
||||||
|
advancedSettings { crawlDeveloperOptions() }
|
||||||
|
}
|
||||||
|
roomList {
|
||||||
|
openRoom(getString(R.string.room_displayname_empty_room)) {
|
||||||
|
val message = "Test view source"
|
||||||
|
postMessage(message)
|
||||||
|
openMessageMenu(message) {
|
||||||
|
viewSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elementRobot.roomList {
|
elementRobot.roomList {
|
||||||
verifyCreatedRoom()
|
verifyCreatedRoom()
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,3 +141,9 @@ class ElementRobot {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
|
private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
|
||||||
|
|
||||||
|
fun ElementRobot.withDeveloperMode(block: ElementRobot.() -> Unit) {
|
||||||
|
settings { toggleDeveloperMode() }
|
||||||
|
block()
|
||||||
|
settings { toggleDeveloperMode() }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.ui.robot
|
||||||
|
|
||||||
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
|
||||||
|
import im.vector.app.R
|
||||||
|
import java.lang.Thread.sleep
|
||||||
|
|
||||||
|
class MessageMenuRobot(
|
||||||
|
var autoClosed: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun viewSource() {
|
||||||
|
clickOn(R.string.view_source)
|
||||||
|
// wait for library
|
||||||
|
sleep(1000)
|
||||||
|
pressBack()
|
||||||
|
autoClosed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editHistory() {
|
||||||
|
clickOn(R.string.message_view_edit_history)
|
||||||
|
pressBack()
|
||||||
|
autoClosed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addQuickReaction(quickReaction: String) {
|
||||||
|
clickOn(quickReaction)
|
||||||
|
autoClosed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addReactionFromEmojiPicker() {
|
||||||
|
clickOn(R.string.message_add_reaction)
|
||||||
|
// Wait for emoji to load, it's async now
|
||||||
|
sleep(2000)
|
||||||
|
clickListItem(R.id.emojiRecyclerView, 4)
|
||||||
|
autoClosed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun edit() {
|
||||||
|
clickOn(R.string.edit)
|
||||||
|
autoClosed = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,21 +17,24 @@
|
||||||
package im.vector.app.ui.robot
|
package im.vector.app.ui.robot
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.Espresso.pressBack
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
||||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn
|
||||||
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
|
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
|
||||||
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
|
|
||||||
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
|
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
|
||||||
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
|
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.espresso.tools.waitUntilViewVisible
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||||
|
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||||
|
import im.vector.app.interactWithSheet
|
||||||
import im.vector.app.waitForView
|
import im.vector.app.waitForView
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
|
|
||||||
|
@ -39,7 +42,9 @@ class RoomDetailRobot {
|
||||||
|
|
||||||
fun postMessage(content: String) {
|
fun postMessage(content: String) {
|
||||||
writeTo(R.id.composerEditText, content)
|
writeTo(R.id.composerEditText, content)
|
||||||
|
waitUntilViewVisible(withId(R.id.sendButton))
|
||||||
clickOn(R.id.sendButton)
|
clickOn(R.id.sendButton)
|
||||||
|
waitUntilViewVisible(withText(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun crawl() {
|
fun crawl() {
|
||||||
|
@ -55,61 +60,54 @@ class RoomDetailRobot {
|
||||||
pressBack()
|
pressBack()
|
||||||
clickMenu(R.id.search)
|
clickMenu(R.id.search)
|
||||||
pressBack()
|
pressBack()
|
||||||
// Long click on the message
|
|
||||||
longClickOnMessageTest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun longClickOnMessageTest() {
|
fun crawlMessage(message: String) {
|
||||||
// Test quick reaction
|
// Test quick reaction
|
||||||
longClickOnMessage()
|
val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
|
||||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
openMessageMenu(message) {
|
||||||
// Add quick reaction
|
addQuickReaction(quickReaction)
|
||||||
clickOn("\uD83D\uDC4D️") // 👍
|
}
|
||||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
|
||||||
|
|
||||||
// Open reactions
|
// Open reactions
|
||||||
longClickOn("\uD83D\uDC4D️") // 👍
|
longClickOn(quickReaction)
|
||||||
// wait for bottom sheet
|
// wait for bottom sheet
|
||||||
pressBack()
|
pressBack()
|
||||||
|
|
||||||
// Test add reaction
|
// Test add reaction
|
||||||
longClickOnMessage()
|
openMessageMenu(message) {
|
||||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
addReactionFromEmojiPicker()
|
||||||
clickOn(R.string.message_add_reaction)
|
}
|
||||||
// Filter
|
|
||||||
// TODO clickMenu(R.id.search)
|
|
||||||
// Wait for emoji to load, it's async now
|
|
||||||
sleep(2000)
|
|
||||||
clickListItem(R.id.emojiRecyclerView, 4)
|
|
||||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
|
||||||
|
|
||||||
// Test Edit mode
|
// Test Edit mode
|
||||||
longClickOnMessage()
|
openMessageMenu(message) {
|
||||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
edit()
|
||||||
clickOn(R.string.edit)
|
}
|
||||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
|
||||||
// TODO Cancel action
|
// TODO Cancel action
|
||||||
writeTo(R.id.composerEditText, "Hello universe!")
|
writeTo(R.id.composerEditText, "Hello universe!")
|
||||||
// Wait a bit for the keyboard layout to update
|
// Wait a bit for the keyboard layout to update
|
||||||
sleep(30)
|
waitUntilViewVisible(withId(R.id.sendButton))
|
||||||
clickOn(R.id.sendButton)
|
clickOn(R.id.sendButton)
|
||||||
// Wait for the UI to update
|
// Wait for the UI to update
|
||||||
sleep(1000)
|
waitUntilViewVisible(withText("Hello universe! (edited)"))
|
||||||
// Open edit history
|
// Open edit history
|
||||||
longClickOnMessage("Hello universe! (edited)")
|
openMessageMenu("Hello universe! (edited)") {
|
||||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
editHistory()
|
||||||
clickOn(R.string.message_view_edit_history)
|
}
|
||||||
pressBack()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun longClickOnMessage(text: String = "Hello world!") {
|
fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) {
|
||||||
Espresso.onView(withId(R.id.timelineRecyclerView))
|
onView(withId(R.id.timelineRecyclerView))
|
||||||
.perform(
|
.perform(
|
||||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
ViewMatchers.hasDescendant(ViewMatchers.withText(text)),
|
ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
|
||||||
ViewActions.longClick()
|
ViewActions.longClick()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
interactWithSheet<MessageActionsBottomSheet>(contentMatcher = withId(R.id.bottomSheetRecyclerView)) {
|
||||||
|
val messageMenuRobot = MessageMenuRobot()
|
||||||
|
block(messageMenuRobot)
|
||||||
|
if (!messageMenuRobot.autoClosed) {
|
||||||
|
pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openSettings(block: RoomSettingsRobot.() -> Unit) {
|
fun openSettings(block: RoomSettingsRobot.() -> Unit) {
|
||||||
|
|
|
@ -17,38 +17,46 @@
|
||||||
package im.vector.app.ui.robot
|
package im.vector.app.ui.robot
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions
|
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions
|
||||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
||||||
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
|
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
|
||||||
|
|
||||||
class RoomListRobot {
|
class RoomListRobot {
|
||||||
|
|
||||||
|
fun openRoom(roomName: String, block: RoomDetailRobot.() -> Unit) {
|
||||||
|
clickOn(roomName)
|
||||||
|
block(RoomDetailRobot())
|
||||||
|
pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
fun verifyCreatedRoom() {
|
fun verifyCreatedRoom() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.roomListView))
|
onView(ViewMatchers.withId(R.id.roomListView))
|
||||||
.perform(
|
.perform(
|
||||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.room_displayname_empty_room)),
|
ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)),
|
||||||
ViewActions.longClick()
|
ViewActions.longClick()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Espresso.pressBack()
|
pressBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newRoom(block: NewRoomRobot.() -> Unit) {
|
fun newRoom(block: NewRoomRobot.() -> Unit) {
|
||||||
BaristaClickInteractions.clickOn(R.id.createGroupRoomButton)
|
clickOn(R.id.createGroupRoomButton)
|
||||||
waitUntilActivityVisible<RoomDirectoryActivity> {
|
waitUntilActivityVisible<RoomDirectoryActivity> {
|
||||||
BaristaVisibilityAssertions.assertDisplayed(R.id.publicRoomsList)
|
BaristaVisibilityAssertions.assertDisplayed(R.id.publicRoomsList)
|
||||||
}
|
}
|
||||||
val newRoomRobot = NewRoomRobot()
|
val newRoomRobot = NewRoomRobot()
|
||||||
block(newRoomRobot)
|
block(newRoomRobot)
|
||||||
if (!newRoomRobot.createdRoom) {
|
if (!newRoomRobot.createdRoom) {
|
||||||
Espresso.pressBack()
|
pressBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
package im.vector.app.ui.robot.settings
|
package im.vector.app.ui.robot.settings
|
||||||
|
|
||||||
import androidx.test.espresso.Espresso.pressBack
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.espresso.tools.clickOnPreference
|
import im.vector.app.espresso.tools.clickOnPreference
|
||||||
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
|
|
||||||
class SettingsAdvancedRobot {
|
class SettingsAdvancedRobot {
|
||||||
|
|
||||||
|
@ -28,20 +31,19 @@ class SettingsAdvancedRobot {
|
||||||
|
|
||||||
clickOnPreference(R.string.settings_push_rules)
|
clickOnPreference(R.string.settings_push_rules)
|
||||||
pressBack()
|
pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO P2 test developer screens
|
fun toggleDeveloperMode() {
|
||||||
// Enable developer mode
|
clickOn(R.string.settings_developer_mode_summary)
|
||||||
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
|
}
|
||||||
|
|
||||||
clickOnPreference(R.string.settings_account_data)
|
fun crawlDeveloperOptions() {
|
||||||
clickOn("m.push_rules")
|
clickOnPreference(R.string.settings_account_data)
|
||||||
pressBack()
|
waitUntilViewVisible(withText("m.push_rules"))
|
||||||
pressBack()
|
clickOn("m.push_rules")
|
||||||
clickOnPreference(R.string.settings_key_requests)
|
pressBack()
|
||||||
pressBack()
|
pressBack()
|
||||||
|
clickOnPreference(R.string.settings_key_requests)
|
||||||
// Disable developer mode
|
pressBack()
|
||||||
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,12 @@ import im.vector.app.clickOnAndGoBack
|
||||||
|
|
||||||
class SettingsRobot {
|
class SettingsRobot {
|
||||||
|
|
||||||
|
fun toggleDeveloperMode() {
|
||||||
|
advancedSettings {
|
||||||
|
toggleDeveloperMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun general(block: SettingsGeneralRobot.() -> Unit) {
|
fun general(block: SettingsGeneralRobot.() -> Unit) {
|
||||||
clickOnAndGoBack(R.string.settings_general_title) { block(SettingsGeneralRobot()) }
|
clickOnAndGoBack(R.string.settings_general_title) { block(SettingsGeneralRobot()) }
|
||||||
}
|
}
|
||||||
|
@ -50,7 +56,9 @@ class SettingsRobot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
|
fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
|
||||||
clickOnAndGoBack(R.string.settings_advanced_settings) { block(SettingsAdvancedRobot()) }
|
clickOnAndGoBack(R.string.settings_advanced_settings) {
|
||||||
|
block(SettingsAdvancedRobot())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun helpAndAbout(block: SettingsHelpRobot.() -> Unit) {
|
fun helpAndAbout(block: SettingsHelpRobot.() -> Unit) {
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
||||||
<receiver android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver">
|
<receiver
|
||||||
|
android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import im.vector.app.core.extensions.singletonEntryPoint
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
|
import im.vector.app.core.platform.PendingIntentCompat
|
||||||
import im.vector.app.core.services.VectorSyncService
|
import im.vector.app.core.services.VectorSyncService
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -67,7 +68,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
|
||||||
putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
|
putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
|
||||||
putExtra(SyncService.EXTRA_PERIODIC, true)
|
putExtra(SyncService.EXTRA_PERIODIC, true)
|
||||||
}
|
}
|
||||||
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val pIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
REQUEST_CODE,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L
|
val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L
|
||||||
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
@ -80,7 +86,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
|
||||||
fun cancelAlarm(context: Context) {
|
fun cancelAlarm(context: Context) {
|
||||||
Timber.v("## Sync: Cancel alarm for background sync")
|
Timber.v("## Sync: Cancel alarm for background sync")
|
||||||
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
|
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
|
||||||
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val pIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
REQUEST_CODE,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
||||||
alarmMgr.cancel(pIntent)
|
alarmMgr.cancel(pIntent)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
android:name="firebase_analytics_collection_deactivated"
|
android:name="firebase_analytics_collection_deactivated"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<service android:name=".gplay.push.fcm.VectorFirebaseMessagingService">
|
<service
|
||||||
|
android:name=".gplay.push.fcm.VectorFirebaseMessagingService"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -201,8 +201,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
resolvedEvent
|
resolvedEvent
|
||||||
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
|
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
|
||||||
?.let {
|
?.let {
|
||||||
notificationDrawerManager.onNotifiableEventReceived(it)
|
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) }
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
package="im.vector.app">
|
package="im.vector.app">
|
||||||
|
|
||||||
<!-- Needed for VOIP call to detect and switch to headset-->
|
<!-- Needed for VOIP call to detect and switch to headset-->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
@ -418,6 +421,22 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/sdk_provider_paths" />
|
android:resource="@xml/sdk_provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<!-- Temporary fix for Android 12. android:exported has to be explicitly set when targeting Android 12
|
||||||
|
Do it for services coming from dependencies - BEGIN -->
|
||||||
|
<service
|
||||||
|
android:name="org.jitsi.meet.sdk.ConnectionService"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge" />
|
||||||
|
<service
|
||||||
|
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge" />
|
||||||
|
<service
|
||||||
|
android:name="androidx.sharetarget.ChooserTargetServiceCompat"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge" />
|
||||||
|
<!-- Temporary fix for Android 12 change - END -->
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
package im.vector.app
|
package im.vector.app
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.utils.BehaviorDataSource
|
import im.vector.app.core.utils.BehaviorDataSource
|
||||||
|
@ -57,7 +56,7 @@ class AppStateHandler @Inject constructor(
|
||||||
private val sessionDataSource: ActiveSessionDataSource,
|
private val sessionDataSource: ActiveSessionDataSource,
|
||||||
private val uiStateRepository: UiStateRepository,
|
private val uiStateRepository: UiStateRepository,
|
||||||
private val activeSessionHolder: ActiveSessionHolder
|
private val activeSessionHolder: ActiveSessionHolder
|
||||||
) : LifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
|
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
|
||||||
|
@ -133,13 +132,11 @@ class AppStateHandler @Inject constructor(
|
||||||
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
|
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
fun entersForeground() {
|
|
||||||
observeActiveSession()
|
observeActiveSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
fun entersBackground() {
|
|
||||||
coroutineScope.coroutineContext.cancelChildren()
|
coroutineScope.coroutineContext.cancelChildren()
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) {
|
when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) {
|
||||||
|
|
|
@ -27,9 +27,8 @@ import android.os.HandlerThread
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import androidx.core.provider.FontRequest
|
import androidx.core.provider.FontRequest
|
||||||
import androidx.core.provider.FontsContractCompat
|
import androidx.core.provider.FontsContractCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||||
|
@ -166,9 +165,8 @@ class VectorApplication :
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(startSyncOnFirstStart)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(startSyncOnFirstStart)
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
fun entersForeground() {
|
|
||||||
Timber.i("App entered foreground")
|
Timber.i("App entered foreground")
|
||||||
FcmHelper.onEnterForeground(appContext, activeSessionHolder)
|
FcmHelper.onEnterForeground(appContext, activeSessionHolder)
|
||||||
activeSessionHolder.getSafeActiveSession()?.also {
|
activeSessionHolder.getSafeActiveSession()?.also {
|
||||||
|
@ -176,8 +174,7 @@ class VectorApplication :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
fun entersBackground() {
|
|
||||||
Timber.i("App entered background") // call persistInfo
|
Timber.i("App entered background") // call persistInfo
|
||||||
notificationDrawerManager.persistInfo()
|
notificationDrawerManager.persistInfo()
|
||||||
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
||||||
|
@ -198,9 +195,8 @@ class VectorApplication :
|
||||||
EmojiManager.install(GoogleEmojiProvider())
|
EmojiManager.install(GoogleEmojiProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
private val startSyncOnFirstStart = object : LifecycleObserver {
|
private val startSyncOnFirstStart = object : DefaultLifecycleObserver {
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
fun onStart() {
|
|
||||||
Timber.i("App process started")
|
Timber.i("App process started")
|
||||||
authenticationService.getLastAuthenticatedSession()?.startSyncing(appContext)
|
authenticationService.getLastAuthenticatedSession()?.startSyncing(appContext)
|
||||||
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
package im.vector.app.core.contacts
|
package im.vector.app.core.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
@ -57,16 +57,20 @@ class ContactsDataSource @Inject constructor(
|
||||||
)
|
)
|
||||||
?.use { cursor ->
|
?.use { cursor ->
|
||||||
if (cursor.count > 0) {
|
if (cursor.count > 0) {
|
||||||
|
val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Contacts._ID) ?: return@use
|
||||||
|
val displayNameColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||||
|
val photoUriColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Data.PHOTO_URI)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
|
val id = cursor.getLong(idColumnIndex)
|
||||||
val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
|
val displayName = cursor.getString(displayNameColumnIndex)
|
||||||
|
|
||||||
val mappedContactBuilder = MappedContactBuilder(
|
val mappedContactBuilder = MappedContactBuilder(
|
||||||
id = id,
|
id = id,
|
||||||
displayName = displayName
|
displayName = displayName
|
||||||
)
|
)
|
||||||
|
|
||||||
cursor.getString(ContactsContract.Data.PHOTO_URI)
|
photoUriColumnIndex
|
||||||
|
?.let { cursor.getString(it) }
|
||||||
?.let { Uri.parse(it) }
|
?.let { Uri.parse(it) }
|
||||||
?.let { mappedContactBuilder.photoURI = it }
|
?.let { mappedContactBuilder.photoURI = it }
|
||||||
|
|
||||||
|
@ -85,12 +89,15 @@ class ContactsDataSource @Inject constructor(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null)
|
null)
|
||||||
?.use { innerCursor ->
|
?.use { cursor ->
|
||||||
while (innerCursor.moveToNext()) {
|
val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.CONTACT_ID) ?: return@use
|
||||||
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
|
val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use
|
||||||
?.let { map[it] }
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||||
|
.let { map[it] }
|
||||||
?: continue
|
?: continue
|
||||||
innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
cursor.getString(phoneNumberColumnIndex)
|
||||||
?.let {
|
?.let {
|
||||||
mappedContactBuilder.msisdns.add(
|
mappedContactBuilder.msisdns.add(
|
||||||
MappedMsisdn(
|
MappedMsisdn(
|
||||||
|
@ -114,14 +121,17 @@ class ContactsDataSource @Inject constructor(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null)
|
null)
|
||||||
?.use { innerCursor ->
|
?.use { cursor ->
|
||||||
while (innerCursor.moveToNext()) {
|
val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Email.CONTACT_ID) ?: return@use
|
||||||
|
val emailColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Email.DATA) ?: return@use
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
// This would allow you get several email addresses
|
// This would allow you get several email addresses
|
||||||
// if the email addresses were stored in an array
|
// if the email addresses were stored in an array
|
||||||
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
|
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||||
?.let { map[it] }
|
.let { map[it] }
|
||||||
?: continue
|
?: continue
|
||||||
innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
|
cursor.getString(emailColumnIndex)
|
||||||
?.let {
|
?.let {
|
||||||
mappedContactBuilder.emails.add(
|
mappedContactBuilder.emails.add(
|
||||||
MappedEmail(
|
MappedEmail(
|
||||||
|
@ -140,16 +150,4 @@ class ContactsDataSource @Inject constructor(
|
||||||
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
|
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
|
||||||
.map { it.build() }
|
.map { it.build() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Cursor.getString(column: String): String? {
|
|
||||||
return getColumnIndex(column)
|
|
||||||
.takeIf { it != -1 }
|
|
||||||
?.let { getString(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cursor.getLong(column: String): Long? {
|
|
||||||
return getColumnIndex(column)
|
|
||||||
.takeIf { it != -1 }
|
|
||||||
?.let { getLong(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,3 +66,7 @@ fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String {
|
||||||
replaceRange(idx, idx, insert)
|
replaceRange(idx, idx, insert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified R> Any?.takeAs(): R? {
|
||||||
|
return takeIf { it is R } as R?
|
||||||
|
}
|
||||||
|
|
|
@ -19,15 +19,17 @@ package im.vector.app.core.intent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
|
|
||||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||||
if (context != null && uri.scheme == "content") {
|
if (context != null && uri.scheme == "content") {
|
||||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
context.contentResolver.query(uri, null, null, null, null)
|
||||||
cursor?.use {
|
?.use { cursor ->
|
||||||
if (it.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
return it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
return cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME)
|
||||||
}
|
?.let { cursor.getString(it) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return uri.path?.substringAfterLast('/')
|
return uri.path?.substringAfterLast('/')
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,58 +18,56 @@ package im.vector.app.core.platform
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
|
|
||||||
fun <T> LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy<T> = LifecycleAwareLazy(this, initializer)
|
fun <T> LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy<T> = LifecycleAwareLazy(this, initializer)
|
||||||
|
|
||||||
private object UninitializedValue
|
private object UninitializedValue
|
||||||
|
|
||||||
class LifecycleAwareLazy<out T>(
|
class LifecycleAwareLazy<out T>(
|
||||||
private val owner: LifecycleOwner,
|
private val owner: LifecycleOwner,
|
||||||
initializer: () -> T
|
initializer: () -> T
|
||||||
) : Lazy<T>, LifecycleObserver {
|
) : Lazy<T>, DefaultLifecycleObserver {
|
||||||
|
|
||||||
private var initializer: (() -> T)? = initializer
|
private var initializer: (() -> T)? = initializer
|
||||||
|
|
||||||
private var _value: Any? = UninitializedValue
|
private var _value: Any? = UninitializedValue
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override val value: T
|
override val value: T
|
||||||
@MainThread
|
@MainThread
|
||||||
get() {
|
get() {
|
||||||
if (_value === UninitializedValue) {
|
if (_value === UninitializedValue) {
|
||||||
_value = initializer!!()
|
_value = initializer!!()
|
||||||
attachToLifecycle()
|
attachToLifecycle()
|
||||||
}
|
}
|
||||||
return _value as T
|
return _value as T
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
_value = UninitializedValue
|
||||||
|
detachFromLifecycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
private fun attachToLifecycle() {
|
||||||
fun resetValue() {
|
if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||||
_value = UninitializedValue
|
throw IllegalStateException("Initialization failed because lifecycle has been destroyed!")
|
||||||
detachFromLifecycle()
|
}
|
||||||
}
|
getLifecycleOwner().lifecycle.addObserver(this)
|
||||||
|
|
||||||
private fun attachToLifecycle() {
|
|
||||||
if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
|
||||||
throw IllegalStateException("Initialization failed because lifecycle has been destroyed!")
|
|
||||||
}
|
}
|
||||||
getLifecycleOwner().lifecycle.addObserver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detachFromLifecycle() {
|
private fun detachFromLifecycle() {
|
||||||
getLifecycleOwner().lifecycle.removeObserver(this)
|
getLifecycleOwner().lifecycle.removeObserver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLifecycleOwner() = when (owner) {
|
private fun getLifecycleOwner() = when (owner) {
|
||||||
is Fragment -> owner.viewLifecycleOwner
|
is Fragment -> owner.viewLifecycleOwner
|
||||||
else -> owner
|
else -> owner
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isInitialized(): Boolean = _value !== UninitializedValue
|
override fun isInitialized(): Boolean = _value !== UninitializedValue
|
||||||
|
|
||||||
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
|
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,17 +14,21 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.call.telecom
|
package im.vector.app.core.platform
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.PendingIntent
|
||||||
import android.telephony.TelephonyManager
|
import android.os.Build
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
object TelecomUtils {
|
object PendingIntentCompat {
|
||||||
|
val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
fun isLineBusy(context: Context): Boolean {
|
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
val telephonyManager = context.getSystemService<TelephonyManager>()
|
PendingIntent.FLAG_MUTABLE
|
||||||
?: return false
|
} else {
|
||||||
return telephonyManager.callState != TelephonyManager.CALL_STATE_IDLE
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.core.platform
|
package im.vector.app.core.platform
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
@ -403,7 +404,12 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
} else {
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
||||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
|
|
|
@ -32,6 +32,7 @@ import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.platform.PendingIntentCompat
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
import im.vector.app.features.settings.BackgroundSyncMode
|
import im.vector.app.features.settings.BackgroundSyncMode
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
||||||
|
@ -199,9 +200,9 @@ private fun Context.rescheduleSyncService(sessionId: String,
|
||||||
startService(intent)
|
startService(intent)
|
||||||
} else {
|
} else {
|
||||||
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
PendingIntent.getForegroundService(this, 0, intent, 0)
|
PendingIntent.getForegroundService(this, 0, intent, PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.getService(this, 0, intent, 0)
|
PendingIntent.getService(this, 0, intent, PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
}
|
}
|
||||||
val firstMillis = System.currentTimeMillis() + syncDelaySeconds * 1000L
|
val firstMillis = System.currentTimeMillis() + syncDelaySeconds * 1000L
|
||||||
val alarmMgr = getSystemService<AlarmManager>()!!
|
val alarmMgr = getSystemService<AlarmManager>()!!
|
||||||
|
|
|
@ -20,9 +20,8 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.facebook.react.bridge.JavaOnlyMap
|
import com.facebook.react.bridge.JavaOnlyMap
|
||||||
import org.jitsi.meet.sdk.BroadcastEmitter
|
import org.jitsi.meet.sdk.BroadcastEmitter
|
||||||
|
@ -42,7 +41,7 @@ sealed class ConferenceEvent(open val data: Map<String, Any>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConferenceEventEmitter(private val context: Context) {
|
class ConferenceEventEmitter(private val context: Context) {
|
||||||
|
|
||||||
fun emitConferenceEnded() {
|
fun emitConferenceEnded() {
|
||||||
val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference())
|
val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference())
|
||||||
|
@ -52,7 +51,7 @@ class ConferenceEventEmitter(private val context: Context) {
|
||||||
|
|
||||||
class ConferenceEventObserver(private val context: Context,
|
class ConferenceEventObserver(private val context: Context,
|
||||||
private val onBroadcastEvent: (ConferenceEvent) -> Unit) :
|
private val onBroadcastEvent: (ConferenceEvent) -> Unit) :
|
||||||
LifecycleObserver {
|
DefaultLifecycleObserver {
|
||||||
|
|
||||||
// See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
|
// See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
|
||||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
@ -61,8 +60,7 @@ class ConferenceEventObserver(private val context: Context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
fun unregisterForBroadcastMessages() {
|
|
||||||
try {
|
try {
|
||||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
|
LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
|
@ -70,8 +68,7 @@ class ConferenceEventObserver(private val context: Context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
fun registerForBroadcastMessages() {
|
|
||||||
val intentFilter = IntentFilter()
|
val intentFilter = IntentFilter()
|
||||||
for (type in BroadcastEvent.Type.values()) {
|
for (type in BroadcastEvent.Type.values()) {
|
||||||
intentFilter.addAction(type.action)
|
intentFilter.addAction(type.action)
|
||||||
|
|
|
@ -17,9 +17,8 @@
|
||||||
package im.vector.app.features.call.webrtc
|
package im.vector.app.features.call.webrtc
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import im.vector.app.ActiveSessionDataSource
|
import im.vector.app.ActiveSessionDataSource
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.core.services.CallService
|
import im.vector.app.core.services.CallService
|
||||||
|
@ -70,7 +69,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
|
||||||
class WebRtcCallManager @Inject constructor(
|
class WebRtcCallManager @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val activeSessionDataSource: ActiveSessionDataSource
|
private val activeSessionDataSource: ActiveSessionDataSource
|
||||||
) : CallListener, LifecycleObserver {
|
) : CallListener,
|
||||||
|
DefaultLifecycleObserver {
|
||||||
|
|
||||||
private val currentSession: Session?
|
private val currentSession: Session?
|
||||||
get() = activeSessionDataSource.currentValue?.orNull()
|
get() = activeSessionDataSource.currentValue?.orNull()
|
||||||
|
@ -133,13 +133,11 @@ class WebRtcCallManager @Inject constructor(
|
||||||
|
|
||||||
private var isInBackground: Boolean = true
|
private var isInBackground: Boolean = true
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
fun entersForeground() {
|
|
||||||
isInBackground = false
|
isInBackground = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
fun entersBackground() {
|
|
||||||
isInBackground = true
|
isInBackground = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,8 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
var recoveryCode: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
var recoveryCodeErrorText: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
|
|
||||||
init {
|
|
||||||
recoveryCode.value = null
|
|
||||||
recoveryCodeErrorText.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========= Actions =========
|
// ========= Actions =========
|
||||||
fun updateCode(newValue: String) {
|
fun updateCode(newValue: String) {
|
||||||
|
|
|
@ -28,13 +28,8 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var passphrase: MutableLiveData<String> = MutableLiveData()
|
var passphrase: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
var passphraseErrorText: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
|
|
||||||
init {
|
|
||||||
passphrase.value = null
|
|
||||||
passphraseErrorText.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========= Actions =========
|
// ========= Actions =========
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
|
||||||
|
|
||||||
lateinit var session: Session
|
lateinit var session: Session
|
||||||
|
|
||||||
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
var keyVersionResult: MutableLiveData<KeysVersionResult?> = MutableLiveData(null)
|
||||||
|
|
||||||
var keySourceModel: MutableLiveData<KeySource> = MutableLiveData()
|
var keySourceModel: MutableLiveData<KeySource> = MutableLiveData()
|
||||||
|
|
||||||
|
@ -69,17 +69,11 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
|
||||||
val navigateEvent: LiveData<LiveEvent<String>>
|
val navigateEvent: LiveData<LiveEvent<String>>
|
||||||
get() = _navigateEvent
|
get() = _navigateEvent
|
||||||
|
|
||||||
var loadingEvent: MutableLiveData<WaitingViewData> = MutableLiveData()
|
var loadingEvent: MutableLiveData<WaitingViewData?> = MutableLiveData(null)
|
||||||
|
|
||||||
var importKeyResult: ImportRoomKeysResult? = null
|
var importKeyResult: ImportRoomKeysResult? = null
|
||||||
var importRoomKeysFinishWithResult: MutableLiveData<LiveEvent<ImportRoomKeysResult>> = MutableLiveData()
|
var importRoomKeysFinishWithResult: MutableLiveData<LiveEvent<ImportRoomKeysResult>> = MutableLiveData()
|
||||||
|
|
||||||
init {
|
|
||||||
keyVersionResult.value = null
|
|
||||||
_keyVersionResultError.value = null
|
|
||||||
loadingEvent.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initSession(session: Session) {
|
fun initSession(session: Session) {
|
||||||
this.session = session
|
this.session = session
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,23 +68,15 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
||||||
// Step 3
|
// Step 3
|
||||||
// Var to ignore events from previous request(s) to generate a recovery key
|
// Var to ignore events from previous request(s) to generate a recovery key
|
||||||
private var currentRequestId: MutableLiveData<Long> = MutableLiveData()
|
private var currentRequestId: MutableLiveData<Long> = MutableLiveData()
|
||||||
var recoveryKey: MutableLiveData<String> = MutableLiveData()
|
var recoveryKey: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
var prepareRecoverFailError: MutableLiveData<Throwable> = MutableLiveData()
|
var prepareRecoverFailError: MutableLiveData<Throwable?> = MutableLiveData(null)
|
||||||
var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
|
var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
|
||||||
var copyHasBeenMade = false
|
var copyHasBeenMade = false
|
||||||
var isCreatingBackupVersion: MutableLiveData<Boolean> = MutableLiveData()
|
var isCreatingBackupVersion: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||||
var creatingBackupError: MutableLiveData<Throwable> = MutableLiveData()
|
var creatingBackupError: MutableLiveData<Throwable?> = MutableLiveData(null)
|
||||||
var keysVersion: MutableLiveData<KeysVersion> = MutableLiveData()
|
var keysVersion: MutableLiveData<KeysVersion> = MutableLiveData()
|
||||||
|
|
||||||
var loadingStatus: MutableLiveData<WaitingViewData> = MutableLiveData()
|
var loadingStatus: MutableLiveData<WaitingViewData?> = MutableLiveData(null)
|
||||||
|
|
||||||
init {
|
|
||||||
recoveryKey.value = null
|
|
||||||
isCreatingBackupVersion.value = false
|
|
||||||
prepareRecoverFailError.value = null
|
|
||||||
creatingBackupError.value = null
|
|
||||||
loadingStatus.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initSession(session: Session) {
|
fun initSession(session: Session) {
|
||||||
this.session = session
|
this.session = session
|
||||||
|
|
|
@ -21,8 +21,10 @@ import android.content.pm.ShortcutManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.pin.PinCodeStore
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
import im.vector.app.features.pin.PinCodeStoreListener
|
import im.vector.app.features.pin.PinCodeStoreListener
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -43,6 +45,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class ShortcutsHandler @Inject constructor(
|
class ShortcutsHandler @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
private val appDispatchers: CoroutineDispatchers,
|
private val appDispatchers: CoroutineDispatchers,
|
||||||
private val shortcutCreator: ShortcutCreator,
|
private val shortcutCreator: ShortcutCreator,
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
@ -72,7 +75,7 @@ class ShortcutsHandler @Inject constructor(
|
||||||
.onCompletion { pinCodeStore.removeListener(this@ShortcutsHandler) }
|
.onCompletion { pinCodeStore.removeListener(this@ShortcutsHandler) }
|
||||||
.onEach { rooms ->
|
.onEach { rooms ->
|
||||||
// Remove dead shortcuts (i.e. deleted rooms)
|
// Remove dead shortcuts (i.e. deleted rooms)
|
||||||
removeDeadShortcut(rooms.map { it.roomId })
|
removeDeadShortcuts(rooms.map { it.roomId })
|
||||||
|
|
||||||
// Create shortcuts
|
// Create shortcuts
|
||||||
createShortcuts(rooms)
|
createShortcuts(rooms)
|
||||||
|
@ -81,7 +84,7 @@ class ShortcutsHandler @Inject constructor(
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDeadShortcut(roomIds: List<String>) {
|
private fun removeDeadShortcuts(roomIds: List<String>) {
|
||||||
val deadShortcutIds = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC)
|
val deadShortcutIds = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC)
|
||||||
.map { it.id }
|
.map { it.id }
|
||||||
.filter { !roomIds.contains(it) }
|
.filter { !roomIds.contains(it) }
|
||||||
|
@ -91,7 +94,11 @@ class ShortcutsHandler @Inject constructor(
|
||||||
ShortcutManagerCompat.removeLongLivedShortcuts(context, deadShortcutIds)
|
ShortcutManagerCompat.removeLongLivedShortcuts(context, deadShortcutIds)
|
||||||
if (isRequestPinShortcutSupported) {
|
if (isRequestPinShortcutSupported) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
context.getSystemService<ShortcutManager>()?.disableShortcuts(deadShortcutIds)
|
ShortcutManagerCompat.disableShortcuts(
|
||||||
|
context,
|
||||||
|
deadShortcutIds,
|
||||||
|
stringProvider.getString(R.string.shortcut_disabled_reason_room_left)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,8 +137,15 @@ class ShortcutsHandler @Inject constructor(
|
||||||
if (isRequestPinShortcutSupported) {
|
if (isRequestPinShortcutSupported) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
context.getSystemService<ShortcutManager>()
|
context.getSystemService<ShortcutManager>()
|
||||||
?.let {
|
?.pinnedShortcuts
|
||||||
it.disableShortcuts(it.pinnedShortcuts.map { pinnedShortcut -> pinnedShortcut.id })
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.map { pinnedShortcut -> pinnedShortcut.id }
|
||||||
|
?.let { shortcutIdsToDisable ->
|
||||||
|
ShortcutManagerCompat.disableShortcuts(
|
||||||
|
context,
|
||||||
|
shortcutIdsToDisable,
|
||||||
|
stringProvider.getString(R.string.shortcut_disabled_reason_sign_out)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2102,12 +2102,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
|
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) }
|
||||||
roomDetailViewModel.handle(RoomDetailAction.AcceptInvite)
|
roomDetailViewModel.handle(RoomDetailAction.AcceptInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRejectInvite() {
|
override fun onRejectInvite() {
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) }
|
||||||
roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
|
roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,15 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.composer
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
|
import androidx.core.view.OnReceiveContentListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
import com.vanniktech.emoji.EmojiEditText
|
import com.vanniktech.emoji.EmojiEditText
|
||||||
|
@ -33,7 +35,7 @@ import im.vector.app.features.html.PillImageSpan
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) :
|
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) :
|
||||||
EmojiEditText(context, attrs, defStyleAttr) {
|
EmojiEditText(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||||
|
@ -43,23 +45,35 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
||||||
val ic = super.onCreateInputConnection(editorInfo) ?: return null
|
var ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
|
val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this) ?: arrayOf("image/*")
|
||||||
|
|
||||||
val callback =
|
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes)
|
||||||
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
|
ic = InputConnectionCompat.createWrapper(this, ic, editorInfo)
|
||||||
val lacksPermission = (flags and
|
|
||||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
val onReceiveContentListener = OnReceiveContentListener { _, payload ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
|
val split = payload.partition { item -> item.uri != null }
|
||||||
try {
|
val uriContent = split.first
|
||||||
inputContentInfo.requestPermission()
|
val remaining = split.second
|
||||||
} catch (e: Exception) {
|
|
||||||
return@OnCommitContentListener false
|
if (uriContent != null) {
|
||||||
}
|
val clip: ClipData = uriContent.clip
|
||||||
}
|
for (i in 0 until clip.itemCount) {
|
||||||
callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
|
val uri = clip.getItemAt(i).uri
|
||||||
|
// ... app-specific logic to handle the URI ...
|
||||||
|
callback?.onRichContentSelected(uri)
|
||||||
}
|
}
|
||||||
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
|
}
|
||||||
|
// Return anything that we didn't handle ourselves. This preserves the default platform
|
||||||
|
// behavior for text and anything else for which we are not implementing custom handling.
|
||||||
|
// Return anything that we didn't handle ourselves. This preserves the default platform
|
||||||
|
// behavior for text and anything else for which we are not implementing custom handling.
|
||||||
|
remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewCompat.setOnReceiveContentListener(this, mimeTypes, onReceiveContentListener)
|
||||||
|
|
||||||
|
return ic
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -482,7 +482,7 @@ class RoomListFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAcceptRoomInvitation(room: RoomSummary) {
|
override fun onAcceptRoomInvitation(room: RoomSummary) {
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||||
roomListViewModel.handle(RoomListAction.AcceptInvitation(room))
|
roomListViewModel.handle(RoomListAction.AcceptInvitation(room))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,7 +495,7 @@ class RoomListFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRejectRoomInvitation(room: RoomSummary) {
|
override fun onRejectRoomInvitation(room: RoomSummary) {
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||||
roomListViewModel.handle(RoomListAction.RejectInvitation(room))
|
roomListViewModel.handle(RoomListAction.RejectInvitation(room))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.takeAs
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||||
|
@ -28,12 +30,15 @@ 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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.isEdition
|
import org.matrix.android.sdk.api.session.events.model.isEdition
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
|
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -49,11 +54,12 @@ import javax.inject.Inject
|
||||||
class NotifiableEventResolver @Inject constructor(
|
class NotifiableEventResolver @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val noticeEventFormatter: NoticeEventFormatter,
|
private val noticeEventFormatter: NoticeEventFormatter,
|
||||||
private val displayableEventFormatter: DisplayableEventFormatter) {
|
private val displayableEventFormatter: DisplayableEventFormatter
|
||||||
|
) {
|
||||||
|
|
||||||
// private val eventDisplay = RiotEventDisplay(context)
|
// private val eventDisplay = RiotEventDisplay(context)
|
||||||
|
|
||||||
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? {
|
suspend fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? {
|
||||||
val roomID = event.roomId ?: return null
|
val roomID = event.roomId ?: return null
|
||||||
val eventId = event.eventId ?: return null
|
val eventId = event.eventId ?: return null
|
||||||
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
|
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
|
||||||
|
@ -89,7 +95,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
|
suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
|
||||||
if (event.getClearType() != EventType.MESSAGE) return null
|
if (event.getClearType() != EventType.MESSAGE) return null
|
||||||
|
|
||||||
// Ignore message edition
|
// Ignore message edition
|
||||||
|
@ -120,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
|
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
|
||||||
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
||||||
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
||||||
|
|
||||||
|
@ -140,6 +146,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
senderName = senderDisplayName,
|
senderName = senderDisplayName,
|
||||||
senderId = event.root.senderId,
|
senderId = event.root.senderId,
|
||||||
body = body.toString(),
|
body = body.toString(),
|
||||||
|
imageUri = event.fetchImageIfPresent(session),
|
||||||
roomId = event.root.roomId!!,
|
roomId = event.root.roomId!!,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
matrixID = session.myUserId
|
matrixID = session.myUserId
|
||||||
|
@ -173,6 +180,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
senderName = senderDisplayName,
|
senderName = senderDisplayName,
|
||||||
senderId = event.root.senderId,
|
senderId = event.root.senderId,
|
||||||
body = body,
|
body = body,
|
||||||
|
imageUri = event.fetchImageIfPresent(session),
|
||||||
roomId = event.root.roomId!!,
|
roomId = event.root.roomId!!,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomIsDirect = room.roomSummary()?.isDirect ?: false,
|
roomIsDirect = room.roomSummary()?.isDirect ?: false,
|
||||||
|
@ -192,6 +200,26 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? {
|
||||||
|
return when {
|
||||||
|
root.isEncrypted() && root.mxDecryptionResult == null -> null
|
||||||
|
root.isImageMessage() -> downloadAndExportImage(session)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? {
|
||||||
|
return kotlin.runCatching {
|
||||||
|
getLastMessageContent()?.takeAs<MessageWithAttachmentContent>()?.let { imageMessage ->
|
||||||
|
val fileService = session.fileService()
|
||||||
|
fileService.downloadFile(imageMessage)
|
||||||
|
fileService.getTemporarySharableURI(imageMessage)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to download and export image for notification")
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
|
private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
|
||||||
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
||||||
val roomId = event.roomId ?: return null
|
val roomId = event.roomId ?: return null
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
|
||||||
data class NotifiableMessageEvent(
|
data class NotifiableMessageEvent(
|
||||||
|
@ -26,6 +27,7 @@ data class NotifiableMessageEvent(
|
||||||
val senderName: String?,
|
val senderName: String?,
|
||||||
val senderId: String?,
|
val senderId: String?,
|
||||||
val body: String?,
|
val body: String?,
|
||||||
|
val imageUri: Uri?,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val roomName: String?,
|
val roomName: String?,
|
||||||
val roomIsDirect: Boolean = false,
|
val roomIsDirect: Boolean = false,
|
||||||
|
|
|
@ -49,26 +49,26 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||||
NotificationUtils.SMART_REPLY_ACTION ->
|
NotificationUtils.SMART_REPLY_ACTION ->
|
||||||
handleSmartReply(intent, context)
|
handleSmartReply(intent, context)
|
||||||
NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
|
NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
|
||||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) }
|
||||||
}
|
}
|
||||||
NotificationUtils.DISMISS_SUMMARY_ACTION ->
|
NotificationUtils.DISMISS_SUMMARY_ACTION ->
|
||||||
notificationDrawerManager.clearAllEvents()
|
notificationDrawerManager.clearAllEvents()
|
||||||
NotificationUtils.MARK_ROOM_READ_ACTION ->
|
NotificationUtils.MARK_ROOM_READ_ACTION ->
|
||||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) }
|
||||||
handleMarkAsRead(it)
|
handleMarkAsRead(roomId)
|
||||||
}
|
}
|
||||||
NotificationUtils.JOIN_ACTION -> {
|
NotificationUtils.JOIN_ACTION -> {
|
||||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(it)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) }
|
||||||
handleJoinRoom(it)
|
handleJoinRoom(roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NotificationUtils.REJECT_ACTION -> {
|
NotificationUtils.REJECT_ACTION -> {
|
||||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(it)
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) }
|
||||||
handleRejectRoom(it)
|
handleRejectRoom(roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,6 +138,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||||
?: context?.getString(R.string.notification_sender_me),
|
?: context?.getString(R.string.notification_sender_me),
|
||||||
senderId = session.myUserId,
|
senderId = session.myUserId,
|
||||||
body = message,
|
body = message,
|
||||||
|
imageUri = null,
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
||||||
roomIsDirect = room.roomSummary()?.isDirect == true,
|
roomIsDirect = room.roomSummary()?.isDirect == true,
|
||||||
|
@ -145,8 +146,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||||
canBeReplaced = false
|
canBeReplaced = false
|
||||||
)
|
)
|
||||||
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) }
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// TODO Error cannot be managed the same way than in Riot
|
// TODO Error cannot be managed the same way than in Riot
|
||||||
|
|
|
@ -93,7 +93,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
#refreshNotificationDrawer() is called.
|
#refreshNotificationDrawer() is called.
|
||||||
Events might be grouped and there might not be one notification per event!
|
Events might be grouped and there might not be one notification per event!
|
||||||
*/
|
*/
|
||||||
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||||
if (!vectorPreferences.areNotificationEnabledForDevice()) {
|
if (!vectorPreferences.areNotificationEnabledForDevice()) {
|
||||||
Timber.i("Notification are disabled for this device")
|
Timber.i("Notification are disabled for this device")
|
||||||
return
|
return
|
||||||
|
@ -105,87 +105,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
} else {
|
} else {
|
||||||
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
||||||
}
|
}
|
||||||
synchronized(queuedEvents) {
|
|
||||||
val existing = queuedEvents.firstOrNull { it.eventId == notifiableEvent.eventId }
|
|
||||||
if (existing != null) {
|
|
||||||
if (existing.canBeReplaced) {
|
|
||||||
// Use the event coming from the event stream as it may contains more info than
|
|
||||||
// the fcm one (like type/content/clear text) (e.g when an encrypted message from
|
|
||||||
// FCM should be update with clear text after a sync)
|
|
||||||
// In this case the message has already been notified, and might have done some noise
|
|
||||||
// So we want the notification to be updated even if it has already been displayed
|
|
||||||
// Use setOnlyAlertOnce to ensure update notification does not interfere with sound
|
|
||||||
// from first notify invocation as outlined in:
|
|
||||||
// https://developer.android.com/training/notify-user/build-notification#Updating
|
|
||||||
queuedEvents.remove(existing)
|
|
||||||
queuedEvents.add(notifiableEvent)
|
|
||||||
} else {
|
|
||||||
// keep the existing one, do not replace
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if this is an edit
|
|
||||||
if (notifiableEvent.editedEventId != null) {
|
|
||||||
// This is an edition
|
|
||||||
val eventBeforeEdition = queuedEvents.firstOrNull {
|
|
||||||
// Edition of an event
|
|
||||||
it.eventId == notifiableEvent.editedEventId ||
|
|
||||||
// or edition of an edition
|
|
||||||
it.editedEventId == notifiableEvent.editedEventId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventBeforeEdition != null) {
|
add(notifiableEvent, seenEventIds)
|
||||||
// Replace the existing notification with the new content
|
|
||||||
queuedEvents.remove(eventBeforeEdition)
|
|
||||||
|
|
||||||
queuedEvents.add(notifiableEvent)
|
|
||||||
} else {
|
|
||||||
// Ignore an edit of a not displayed event in the notification drawer
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not an edit
|
|
||||||
if (seenEventIds.contains(notifiableEvent.eventId)) {
|
|
||||||
// we've already seen the event, lets skip
|
|
||||||
Timber.d("onNotifiableEventReceived(): skipping event, already seen")
|
|
||||||
} else {
|
|
||||||
seenEventIds.put(notifiableEvent.eventId)
|
|
||||||
queuedEvents.add(notifiableEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEventRedacted(eventId: String) {
|
|
||||||
synchronized(queuedEvents) {
|
|
||||||
queuedEvents.replace(eventId) {
|
|
||||||
when (it) {
|
|
||||||
is InviteNotifiableEvent -> it.copy(isRedacted = true)
|
|
||||||
is NotifiableMessageEvent -> it.copy(isRedacted = true)
|
|
||||||
is SimpleNotifiableEvent -> it.copy(isRedacted = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all known events and refresh the notification drawer
|
* Clear all known events and refresh the notification drawer
|
||||||
*/
|
*/
|
||||||
fun clearAllEvents() {
|
fun clearAllEvents() {
|
||||||
synchronized(queuedEvents) {
|
updateEvents { it.clear() }
|
||||||
queuedEvents.clear()
|
|
||||||
}
|
|
||||||
refreshNotificationDrawer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Clear all known message events for this room */
|
|
||||||
fun clearMessageEventOfRoom(roomId: String?) {
|
|
||||||
Timber.v("clearMessageEventOfRoom $roomId")
|
|
||||||
if (roomId != null) {
|
|
||||||
val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
|
|
||||||
if (shouldUpdate) {
|
|
||||||
refreshNotificationDrawer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -193,32 +121,36 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||||
*/
|
*/
|
||||||
fun setCurrentRoom(roomId: String?) {
|
fun setCurrentRoom(roomId: String?) {
|
||||||
var hasChanged: Boolean
|
updateEvents {
|
||||||
synchronized(queuedEvents) {
|
val hasChanged = roomId != currentRoomId
|
||||||
hasChanged = roomId != currentRoomId
|
|
||||||
currentRoomId = roomId
|
currentRoomId = roomId
|
||||||
}
|
if (hasChanged && roomId != null) {
|
||||||
if (hasChanged) {
|
it.clearMessagesForRoom(roomId)
|
||||||
clearMessageEventOfRoom(roomId)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearMemberShipNotificationForRoom(roomId: String) {
|
fun notificationStyleChanged() {
|
||||||
val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId }
|
updateEvents {
|
||||||
if (shouldUpdate) {
|
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
||||||
refreshNotificationDrawerBg()
|
if (newSettings != useCompleteNotificationFormat) {
|
||||||
|
// Settings has changed, remove all current notifications
|
||||||
|
notificationDisplayer.cancelAllNotifications()
|
||||||
|
useCompleteNotificationFormat = newSettings
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean {
|
fun updateEvents(action: NotificationDrawerManager.(NotificationEventQueue) -> Unit) {
|
||||||
return synchronized(queuedEvents) {
|
synchronized(queuedEvents) {
|
||||||
queuedEvents.removeAll(predicate)
|
action(this, queuedEvents)
|
||||||
}
|
}
|
||||||
|
refreshNotificationDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var firstThrottler = FirstThrottler(200)
|
private var firstThrottler = FirstThrottler(200)
|
||||||
|
|
||||||
fun refreshNotificationDrawer() {
|
private fun refreshNotificationDrawer() {
|
||||||
// Implement last throttler
|
// Implement last throttler
|
||||||
val canHandle = firstThrottler.canHandle()
|
val canHandle = firstThrottler.canHandle()
|
||||||
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
|
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
|
||||||
|
@ -239,18 +171,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun refreshNotificationDrawerBg() {
|
private fun refreshNotificationDrawerBg() {
|
||||||
Timber.v("refreshNotificationDrawerBg()")
|
Timber.v("refreshNotificationDrawerBg()")
|
||||||
|
|
||||||
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
|
||||||
if (newSettings != useCompleteNotificationFormat) {
|
|
||||||
// Settings has changed, remove all current notifications
|
|
||||||
notificationDisplayer.cancelAllNotifications()
|
|
||||||
useCompleteNotificationFormat = newSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
val eventsToRender = synchronized(queuedEvents) {
|
val eventsToRender = synchronized(queuedEvents) {
|
||||||
notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also {
|
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, renderedEvents).also {
|
||||||
queuedEvents.clear()
|
queuedEvents.clearAndAdd(it.onlyKeptEvents())
|
||||||
queuedEvents.addAll(it.onlyKeptEvents())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
if (!file.exists()) file.createNewFile()
|
if (!file.exists()) file.createNewFile()
|
||||||
FileOutputStream(file).use {
|
FileOutputStream(file).use {
|
||||||
currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it)
|
currentSession?.securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## Failed to save cached notification info")
|
Timber.e(e, "## Failed to save cached notification info")
|
||||||
|
@ -294,21 +217,21 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadEventInfo(): MutableList<NotifiableEvent> {
|
private fun loadEventInfo(): NotificationEventQueue {
|
||||||
try {
|
try {
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.inputStream().use {
|
file.inputStream().use {
|
||||||
val events: ArrayList<NotifiableEvent>? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
val events: ArrayList<NotifiableEvent>? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
||||||
if (events != null) {
|
if (events != null) {
|
||||||
return events.toMutableList()
|
return NotificationEventQueue(events.toMutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## Failed to load cached notification info")
|
Timber.e(e, "## Failed to load cached notification info")
|
||||||
}
|
}
|
||||||
return ArrayList()
|
return NotificationEventQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteCachedRoomNotifications() {
|
private fun deleteCachedRoomNotifications() {
|
||||||
|
@ -330,11 +253,3 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
|
|
||||||
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
|
||||||
if (indexToReplace == -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
set(indexToReplace, block(get(indexToReplace)))
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class NotificationEventQueue(
|
||||||
|
private val queue: MutableList<NotifiableEvent> = mutableListOf()
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun markRedacted(eventIds: List<String>) {
|
||||||
|
eventIds.forEach { redactedId ->
|
||||||
|
queue.replace(redactedId) {
|
||||||
|
when (it) {
|
||||||
|
is InviteNotifiableEvent -> it.copy(isRedacted = true)
|
||||||
|
is NotifiableMessageEvent -> it.copy(isRedacted = true)
|
||||||
|
is SimpleNotifiableEvent -> it.copy(isRedacted = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncRoomEvents(roomsLeft: Collection<String>, roomsJoined: Collection<String>) {
|
||||||
|
if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) {
|
||||||
|
queue.removeAll {
|
||||||
|
when (it) {
|
||||||
|
is NotifiableMessageEvent -> roomsLeft.contains(it.roomId)
|
||||||
|
is InviteNotifiableEvent -> roomsLeft.contains(it.roomId) || roomsJoined.contains(it.roomId)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty() = queue.isEmpty()
|
||||||
|
|
||||||
|
fun clearAndAdd(events: List<NotifiableEvent>) {
|
||||||
|
queue.clear()
|
||||||
|
queue.addAll(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
queue.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(notifiableEvent: NotifiableEvent, seenEventIds: CircularCache<String>) {
|
||||||
|
val existing = findExistingById(notifiableEvent)
|
||||||
|
val edited = findEdited(notifiableEvent)
|
||||||
|
when {
|
||||||
|
existing != null -> {
|
||||||
|
if (existing.canBeReplaced) {
|
||||||
|
// Use the event coming from the event stream as it may contains more info than
|
||||||
|
// the fcm one (like type/content/clear text) (e.g when an encrypted message from
|
||||||
|
// FCM should be update with clear text after a sync)
|
||||||
|
// In this case the message has already been notified, and might have done some noise
|
||||||
|
// So we want the notification to be updated even if it has already been displayed
|
||||||
|
// Use setOnlyAlertOnce to ensure update notification does not interfere with sound
|
||||||
|
// from first notify invocation as outlined in:
|
||||||
|
// https://developer.android.com/training/notify-user/build-notification#Updating
|
||||||
|
replace(replace = existing, with = notifiableEvent)
|
||||||
|
} else {
|
||||||
|
// keep the existing one, do not replace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edited != null -> {
|
||||||
|
// Replace the existing notification with the new content
|
||||||
|
replace(replace = edited, with = notifiableEvent)
|
||||||
|
}
|
||||||
|
seenEventIds.contains(notifiableEvent.eventId) -> {
|
||||||
|
// we've already seen the event, lets skip
|
||||||
|
Timber.d("onNotifiableEventReceived(): skipping event, already seen")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
seenEventIds.put(notifiableEvent.eventId)
|
||||||
|
queue.add(notifiableEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findExistingById(notifiableEvent: NotifiableEvent): NotifiableEvent? {
|
||||||
|
return queue.firstOrNull { it.eventId == notifiableEvent.eventId }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findEdited(notifiableEvent: NotifiableEvent): NotifiableEvent? {
|
||||||
|
return notifiableEvent.editedEventId?.let { editedId ->
|
||||||
|
queue.firstOrNull {
|
||||||
|
it.eventId == editedId || it.editedEventId == editedId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replace(replace: NotifiableEvent, with: NotifiableEvent) {
|
||||||
|
queue.remove(replace)
|
||||||
|
queue.add(with)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearMemberShipNotificationForRoom(roomId: String) {
|
||||||
|
Timber.v("clearMemberShipOfRoom $roomId")
|
||||||
|
queue.removeAll { it is InviteNotifiableEvent && it.roomId == roomId }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearMessagesForRoom(roomId: String) {
|
||||||
|
Timber.v("clearMessageEventOfRoom $roomId")
|
||||||
|
queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rawEvents(): List<NotifiableEvent> = queue
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
|
||||||
|
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
||||||
|
if (indexToReplace == -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set(indexToReplace, block(get(indexToReplace)))
|
||||||
|
}
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||||
|
@ -104,7 +103,7 @@ class NotificationFactory @Inject constructor(
|
||||||
|
|
||||||
sealed interface RoomNotification {
|
sealed interface RoomNotification {
|
||||||
data class Removed(val roomId: String) : RoomNotification
|
data class Removed(val roomId: String) : RoomNotification
|
||||||
data class Message(val notification: Notification, val shortcutInfo: ShortcutInfoCompat?, val meta: Meta) : RoomNotification {
|
data class Message(val notification: Notification, val meta: Meta) : RoomNotification {
|
||||||
data class Meta(
|
data class Meta(
|
||||||
val summaryLine: CharSequence,
|
val summaryLine: CharSequence,
|
||||||
val messageCount: Int,
|
val messageCount: Int,
|
||||||
|
|
|
@ -17,7 +17,6 @@ package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID
|
||||||
|
@ -63,9 +62,6 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer
|
||||||
}
|
}
|
||||||
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
|
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
|
||||||
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
|
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
|
||||||
wrapper.shortcutInfo?.let {
|
|
||||||
ShortcutManagerCompat.pushDynamicShortcut(appContext, it)
|
|
||||||
}
|
|
||||||
notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification)
|
notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.createIgnoredUri
|
import im.vector.app.core.extensions.createIgnoredUri
|
||||||
|
import im.vector.app.core.platform.PendingIntentCompat
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.services.CallService
|
import im.vector.app.core.services.CallService
|
||||||
import im.vector.app.core.utils.startNotificationChannelSettingsIntent
|
import im.vector.app.core.utils.startNotificationChannelSettingsIntent
|
||||||
|
@ -227,7 +228,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
// build the pending intent go to the home screen if this is clicked.
|
// build the pending intent go to the home screen if this is clicked.
|
||||||
val i = HomeActivity.newIntent(context)
|
val i = HomeActivity.newIntent(context)
|
||||||
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
val pi = PendingIntent.getActivity(context, 0, i, 0)
|
val pi = PendingIntent.getActivity(context, 0, i, PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||||
|
|
||||||
|
@ -320,16 +321,23 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
data = createIgnoredUri(call.callId)
|
data = createIgnoredUri(call.callId)
|
||||||
}
|
}
|
||||||
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
|
val contentPendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
contentIntent,
|
||||||
|
PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
val answerCallPendingIntent = TaskStackBuilder.create(context)
|
val answerCallPendingIntent = TaskStackBuilder.create(context)
|
||||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||||
.addNextIntent(VectorCallActivity.newIntent(
|
.addNextIntent(
|
||||||
context = context,
|
VectorCallActivity.newIntent(
|
||||||
call = call,
|
context = context,
|
||||||
mode = VectorCallActivity.INCOMING_ACCEPT)
|
call = call,
|
||||||
|
mode = VectorCallActivity.INCOMING_ACCEPT
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
||||||
|
|
||||||
|
@ -338,7 +346,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||||
getActionText(R.string.call_notification_reject, R.attr.colorError),
|
getActionText(R.string.call_notification_reject, R.attr.colorError),
|
||||||
rejectCallPendingIntent)
|
rejectCallPendingIntent
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.addAction(
|
builder.addAction(
|
||||||
|
@ -381,7 +390,12 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
data = createIgnoredUri(call.callId)
|
data = createIgnoredUri(call.callId)
|
||||||
}
|
}
|
||||||
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
|
val contentPendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
contentIntent,
|
||||||
|
PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
||||||
|
|
||||||
|
@ -390,7 +404,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||||
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
||||||
rejectCallPendingIntent)
|
rejectCallPendingIntent
|
||||||
|
)
|
||||||
)
|
)
|
||||||
builder.setContentIntent(contentPendingIntent)
|
builder.setContentIntent(contentPendingIntent)
|
||||||
|
|
||||||
|
@ -431,13 +446,14 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||||
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
||||||
rejectCallPendingIntent)
|
rejectCallPendingIntent
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||||
.addNextIntent(VectorCallActivity.newIntent(context, call, null))
|
.addNextIntent(VectorCallActivity.newIntent(context, call, null))
|
||||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
builder.setContentIntent(contentPendingIntent)
|
builder.setContentIntent(contentPendingIntent)
|
||||||
|
|
||||||
|
@ -453,7 +469,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
context,
|
context,
|
||||||
System.currentTimeMillis().toInt(),
|
System.currentTimeMillis().toInt(),
|
||||||
rejectCallActionReceiver,
|
rejectCallActionReceiver,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,7 +515,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||||
.addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId)))
|
.addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId)))
|
||||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
builder.setContentIntent(contentPendingIntent)
|
builder.setContentIntent(contentPendingIntent)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
@ -517,7 +533,10 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
).let {
|
).let {
|
||||||
setContentIntent(it)
|
setContentIntent(it)
|
||||||
}
|
}
|
||||||
|
@ -587,8 +606,12 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
markRoomReadIntent.action = MARK_ROOM_READ_ACTION
|
markRoomReadIntent.action = MARK_ROOM_READ_ACTION
|
||||||
markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId)
|
markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId)
|
||||||
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
||||||
val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent,
|
val markRoomReadPendingIntent = PendingIntent.getBroadcast(
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
markRoomReadIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
NotificationCompat.Action.Builder(R.drawable.ic_material_done_all_white,
|
NotificationCompat.Action.Builder(R.drawable.ic_material_done_all_white,
|
||||||
stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent)
|
stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent)
|
||||||
|
@ -624,8 +647,12 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
||||||
intent.action = DISMISS_ROOM_NOTIF_ACTION
|
intent.action = DISMISS_ROOM_NOTIF_ACTION
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context.applicationContext,
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
context.applicationContext,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
setDeleteIntent(pendingIntent)
|
setDeleteIntent(pendingIntent)
|
||||||
}
|
}
|
||||||
.setTicker(tickerText)
|
.setTicker(tickerText)
|
||||||
|
@ -655,31 +682,41 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
rejectIntent.action = REJECT_ACTION
|
rejectIntent.action = REJECT_ACTION
|
||||||
rejectIntent.data = createIgnoredUri("$roomId&$matrixId")
|
rejectIntent.data = createIgnoredUri("$roomId&$matrixId")
|
||||||
rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||||
val rejectIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), rejectIntent,
|
val rejectIntentPendingIntent = PendingIntent.getBroadcast(
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
rejectIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.vector_notification_reject_invitation,
|
R.drawable.vector_notification_reject_invitation,
|
||||||
stringProvider.getString(R.string.reject),
|
stringProvider.getString(R.string.reject),
|
||||||
rejectIntentPendingIntent)
|
rejectIntentPendingIntent
|
||||||
|
)
|
||||||
|
|
||||||
// offer to type a quick accept button
|
// offer to type a quick accept button
|
||||||
val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java)
|
val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||||
joinIntent.action = JOIN_ACTION
|
joinIntent.action = JOIN_ACTION
|
||||||
joinIntent.data = createIgnoredUri("$roomId&$matrixId")
|
joinIntent.data = createIgnoredUri("$roomId&$matrixId")
|
||||||
joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||||
val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent,
|
val joinIntentPendingIntent = PendingIntent.getBroadcast(
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
joinIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.vector_notification_accept_invitation,
|
R.drawable.vector_notification_accept_invitation,
|
||||||
stringProvider.getString(R.string.join),
|
stringProvider.getString(R.string.join),
|
||||||
joinIntentPendingIntent)
|
joinIntentPendingIntent
|
||||||
|
)
|
||||||
|
|
||||||
val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId)
|
val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId)
|
||||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
|
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
|
||||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
|
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||||
|
|
||||||
if (inviteNotifiableEvent.noisy) {
|
if (inviteNotifiableEvent.noisy) {
|
||||||
// Compat
|
// Compat
|
||||||
|
@ -718,7 +755,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||||
contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId)
|
contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId)
|
||||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
|
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||||
|
|
||||||
if (simpleNotifiableEvent.noisy) {
|
if (simpleNotifiableEvent.noisy) {
|
||||||
// Compat
|
// Compat
|
||||||
|
@ -745,14 +782,22 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
return TaskStackBuilder.create(context)
|
return TaskStackBuilder.create(context)
|
||||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||||
.addNextIntent(roomIntentTap)
|
.addNextIntent(roomIntentTap)
|
||||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
.getPendingIntent(
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
|
private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
|
||||||
val intent = HomeActivity.newIntent(context, clearNotification = true)
|
val intent = HomeActivity.newIntent(context, clearNotification = true)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
intent.data = createIgnoredUri("tapSummary")
|
intent.data = createIgnoredUri("tapSummary")
|
||||||
return PendingIntent.getActivity(context, Random.nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
Random.nextInt(1000),
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -769,8 +814,13 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
intent.action = SMART_REPLY_ACTION
|
intent.action = SMART_REPLY_ACTION
|
||||||
intent.data = createIgnoredUri(roomId)
|
intent.data = createIgnoredUri(roomId)
|
||||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||||
return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent,
|
return PendingIntent.getBroadcast(
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
intent,
|
||||||
|
// PendingIntents attached to actions with remote inputs must be mutable
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
TODO
|
TODO
|
||||||
|
@ -783,7 +833,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
// the action must be unique else the parameters are ignored
|
// the action must be unique else the parameters are ignored
|
||||||
quickReplyIntent.action = QUICK_LAUNCH_ACTION
|
quickReplyIntent.action = QUICK_LAUNCH_ACTION
|
||||||
quickReplyIntent.data = createIgnoredUri($roomId")
|
quickReplyIntent.data = createIgnoredUri($roomId")
|
||||||
return PendingIntent.getActivity(context, 0, quickReplyIntent, 0)
|
return PendingIntent.getActivity(context, 0, quickReplyIntent, PendingIntentCompat.FLAG_IMMUTABLE)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
@ -837,8 +887,12 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||||
intent.action = DISMISS_SUMMARY_ACTION
|
intent.action = DISMISS_SUMMARY_ACTION
|
||||||
intent.data = createIgnoredUri("deleteSummary")
|
intent.data = createIgnoredUri("deleteSummary")
|
||||||
return PendingIntent.getBroadcast(context.applicationContext,
|
return PendingIntent.getBroadcast(
|
||||||
0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
context.applicationContext,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
|
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
|
||||||
|
@ -875,7 +929,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
testActionIntent,
|
testActionIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
|
|
|
@ -16,10 +16,15 @@
|
||||||
|
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.pushrules.Action
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||||
|
import org.matrix.android.sdk.api.pushrules.getActions
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -31,45 +36,36 @@ class PushRuleTriggerListener @Inject constructor(
|
||||||
) : PushRuleService.PushRuleListener {
|
) : PushRuleService.PushRuleListener {
|
||||||
|
|
||||||
private var session: Session? = null
|
private var session: Session? = null
|
||||||
|
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
override fun onMatchRule(event: Event, actions: List<Action>) {
|
override fun onEvents(pushEvents: PushEvents) {
|
||||||
Timber.v("Push rule match for event ${event.eventId}")
|
scope.launch {
|
||||||
val safeSession = session ?: return Unit.also {
|
session?.let { session ->
|
||||||
Timber.e("Called without active session")
|
val notifiableEvents = createNotifiableEvents(pushEvents, session)
|
||||||
|
notificationDrawerManager.updateEvents { queuedEvents ->
|
||||||
|
notifiableEvents.forEach { notifiableEvent ->
|
||||||
|
queuedEvents.onNotifiableEventReceived(notifiableEvent)
|
||||||
|
}
|
||||||
|
queuedEvents.syncRoomEvents(roomsLeft = pushEvents.roomsLeft, roomsJoined = pushEvents.roomsJoined)
|
||||||
|
queuedEvents.markRedacted(pushEvents.redactedEventIds)
|
||||||
|
}
|
||||||
|
} ?: Timber.e("Called without active session")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val notificationAction = actions.toNotificationAction()
|
private suspend fun createNotifiableEvents(pushEvents: PushEvents, session: Session): List<NotifiableEvent> {
|
||||||
if (notificationAction.shouldNotify) {
|
return pushEvents.matchedEvents.mapNotNull { (event, pushRule) ->
|
||||||
val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank())
|
Timber.v("Push rule match for event ${event.eventId}")
|
||||||
if (notifiableEvent == null) {
|
val action = pushRule.getActions().toNotificationAction()
|
||||||
Timber.v("## Failed to resolve event")
|
if (action.shouldNotify) {
|
||||||
// TODO
|
resolver.resolveEvent(event, session, isNoisy = !action.soundName.isNullOrBlank())
|
||||||
} else {
|
} else {
|
||||||
Timber.v("New event to notify")
|
Timber.v("Matched push rule is set to not notify")
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.v("Matched push rule is set to not notify")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRoomLeft(roomId: String) {
|
|
||||||
notificationDrawerManager.clearMessageEventOfRoom(roomId)
|
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRoomJoined(roomId: String) {
|
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEventRedacted(redactedEventId: String) {
|
|
||||||
notificationDrawerManager.onEventRedacted(redactedEventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun batchFinish() {
|
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startWithSession(session: Session) {
|
fun startWithSession(session: Session) {
|
||||||
if (this.session != null) {
|
if (this.session != null) {
|
||||||
stop()
|
stop()
|
||||||
|
@ -79,6 +75,7 @@ class PushRuleTriggerListener @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
scope.coroutineContext.cancelChildren(CancellationException("PushRuleTriggerListener stopping"))
|
||||||
session?.removePushRuleListener(this)
|
session?.removePushRuleListener(this)
|
||||||
session = null
|
session = null
|
||||||
notificationDrawerManager.clearAllEvents()
|
notificationDrawerManager.clearAllEvents()
|
||||||
|
|
|
@ -18,14 +18,10 @@ package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
|
||||||
import me.gujun.android.span.Span
|
import me.gujun.android.span.Span
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -61,17 +57,6 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val largeBitmap = getRoomBitmap(events)
|
val largeBitmap = getRoomBitmap(events)
|
||||||
val shortcutInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val openRoomIntent = RoomDetailActivity.shortcutIntent(appContext, roomId)
|
|
||||||
ShortcutInfoCompat.Builder(appContext, roomId)
|
|
||||||
.setLongLived(true)
|
|
||||||
.setIntent(openRoomIntent)
|
|
||||||
.setShortLabel(roomName)
|
|
||||||
.setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(events.last().senderAvatarPath))
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastMessageTimestamp = events.last().timestamp
|
val lastMessageTimestamp = events.last().timestamp
|
||||||
val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||||
|
@ -96,7 +81,6 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
userDisplayName,
|
userDisplayName,
|
||||||
tickerText
|
tickerText
|
||||||
),
|
),
|
||||||
shortcutInfo,
|
|
||||||
meta
|
meta
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +98,14 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
||||||
else -> addMessage(event.body, event.timestamp, senderPerson)
|
else -> {
|
||||||
|
val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message ->
|
||||||
|
event.imageUri?.let {
|
||||||
|
message.setData("image/", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addMessage(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,10 @@
|
||||||
package im.vector.app.features.pin
|
package im.vector.app.features.pin
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -41,7 +40,7 @@ private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L
|
||||||
class PinLocker @Inject constructor(
|
class PinLocker @Inject constructor(
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
private val vectorPreferences: VectorPreferences
|
private val vectorPreferences: VectorPreferences
|
||||||
) : LifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
enum class State {
|
enum class State {
|
||||||
// App is locked, can be unlock
|
// App is locked, can be unlock
|
||||||
|
@ -87,16 +86,14 @@ class PinLocker @Inject constructor(
|
||||||
computeState()
|
computeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
fun entersForeground() {
|
|
||||||
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
|
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
|
||||||
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod()
|
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod()
|
||||||
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked")
|
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked")
|
||||||
computeState()
|
computeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
fun entersBackground() {
|
|
||||||
Timber.v("App enters background")
|
Timber.v("App enters background")
|
||||||
entersBackgroundTs = SystemClock.elapsedRealtime()
|
entersBackgroundTs = SystemClock.elapsedRealtime()
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,8 +61,8 @@ class CreatePollFragment @Inject constructor(
|
||||||
viewModel.handle(CreatePollAction.OnCreatePoll)
|
viewModel.handle(CreatePollAction.OnCreatePoll)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.subscribe(this) {
|
viewModel.onEach(CreatePollViewState::canCreatePoll) { canCreatePoll ->
|
||||||
views.createPollButton.isEnabled = it.canCreatePoll
|
views.createPollButton.isEnabled = canCreatePoll
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
|
|
|
@ -46,7 +46,7 @@ class RageShake @Inject constructor(private val activity: FragmentActivity,
|
||||||
|
|
||||||
shakeDetector = ShakeDetector(this).apply {
|
shakeDetector = ShakeDetector(this).apply {
|
||||||
setSensitivity(vectorPreferences.getRageshakeSensitivity())
|
setSensitivity(vectorPreferences.getRageshakeSensitivity())
|
||||||
start(sensorManager)
|
start(sensorManager, SensorManager.SENSOR_DELAY_GAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
|
|
||||||
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
||||||
// Refresh the drawer for an immediate effect of this change
|
// Refresh the drawer for an immediate effect of this change
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
notificationDrawerManager.notificationStyleChanged()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
abstract class AbstractVoiceRecorder(
|
abstract class AbstractVoiceRecorder(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
private val filenameExt: String
|
private val filenameExt: String
|
||||||
) : VoiceRecorder {
|
) : VoiceRecorder {
|
||||||
private val outputDirectory: File by lazy {
|
private val outputDirectory: File by lazy {
|
||||||
|
@ -39,7 +39,7 @@ abstract class AbstractVoiceRecorder(
|
||||||
abstract fun convertFile(recordedFile: File?): File?
|
abstract fun convertFile(recordedFile: File?): File?
|
||||||
|
|
||||||
private fun init() {
|
private fun init() {
|
||||||
MediaRecorder().let {
|
createMediaRecorder().let {
|
||||||
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
|
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
|
||||||
setOutputFormat(it)
|
setOutputFormat(it)
|
||||||
it.setAudioEncodingBitRate(24000)
|
it.setAudioEncodingBitRate(24000)
|
||||||
|
@ -48,6 +48,15 @@ abstract class AbstractVoiceRecorder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createMediaRecorder(): MediaRecorder {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
MediaRecorder(context)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaRecorder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun startRecord() {
|
override fun startRecord() {
|
||||||
init()
|
init()
|
||||||
outputFile = File(outputDirectory, "Voice message.$filenameExt")
|
outputFile = File(outputDirectory, "Voice message.$filenameExt")
|
||||||
|
|
|
@ -69,11 +69,11 @@
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
app:layout_constraintBottom_toTopOf="@id/createPollButton"
|
app:layout_constraintBottom_toTopOf="@id/createPollButton"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
||||||
tools:listitem="@layout/item_profile_action" />
|
tools:listitem="@layout/item_form_text_input_with_delete" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/createPollButton"
|
android:id="@+id/createPollButton"
|
||||||
style="@style/Widget.Vector.Button.CreatePoll"
|
style="@style/Widget.Vector.Button.CallToAction"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="56dp"
|
android:layout_height="56dp"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:text="@string/voice_message_release_to_send_toast"
|
tools:text="@string/create_poll_empty_question_error"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/formTextInputTextInputLayout"
|
android:id="@+id/formTextInputTextInputLayout"
|
||||||
style="@style/Widget.Vector.EditText.Form"
|
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/formTextInputTextInputLayout"
|
android:id="@+id/formTextInputTextInputLayout"
|
||||||
style="@style/Widget.Vector.EditText.Form"
|
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
|
|
@ -175,7 +175,7 @@
|
||||||
android:weightSum="3">
|
android:weightSum="3">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1080,6 +1080,9 @@
|
||||||
<string name="room_settings_forget">Forget</string>
|
<string name="room_settings_forget">Forget</string>
|
||||||
<string name="room_settings_add_homescreen_shortcut">Add to Home screen</string>
|
<string name="room_settings_add_homescreen_shortcut">Add to Home screen</string>
|
||||||
|
|
||||||
|
<string name="shortcut_disabled_reason_room_left">The room has been left!</string>
|
||||||
|
<string name="shortcut_disabled_reason_sign_out">The session has been signed out!</string>
|
||||||
|
|
||||||
<!-- home sliding menu -->
|
<!-- home sliding menu -->
|
||||||
<string name="room_sliding_menu_messages">Messages</string>
|
<string name="room_sliding_menu_messages">Messages</string>
|
||||||
<string name="room_sliding_menu_settings">Settings</string>
|
<string name="room_sliding_menu_settings">Settings</string>
|
||||||
|
|
|
@ -19,6 +19,9 @@ package im.vector.app.features.notifications
|
||||||
import im.vector.app.features.notifications.ProcessedEvent.Type
|
import im.vector.app.features.notifications.ProcessedEvent.Type
|
||||||
import im.vector.app.test.fakes.FakeAutoAcceptInvites
|
import im.vector.app.test.fakes.FakeAutoAcceptInvites
|
||||||
import im.vector.app.test.fakes.FakeOutdatedEventDetector
|
import im.vector.app.test.fakes.FakeOutdatedEventDetector
|
||||||
|
import im.vector.app.test.fixtures.aNotifiableMessageEvent
|
||||||
|
import im.vector.app.test.fixtures.aSimpleNotifiableEvent
|
||||||
|
import im.vector.app.test.fixtures.anInviteNotifiableEvent
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
@ -145,48 +148,3 @@ class NotifiableEventProcessorTest {
|
||||||
ProcessedEvent(it.first, it.second)
|
ProcessedEvent(it.first, it.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aSimpleNotifiableEvent(eventId: String, type: String? = null) = SimpleNotifiableEvent(
|
|
||||||
matrixID = null,
|
|
||||||
eventId = eventId,
|
|
||||||
editedEventId = null,
|
|
||||||
noisy = false,
|
|
||||||
title = "title",
|
|
||||||
description = "description",
|
|
||||||
type = type,
|
|
||||||
timestamp = 0,
|
|
||||||
soundName = null,
|
|
||||||
canBeReplaced = false,
|
|
||||||
isRedacted = false
|
|
||||||
)
|
|
||||||
|
|
||||||
fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent(
|
|
||||||
matrixID = null,
|
|
||||||
eventId = "event-id",
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = "a room name",
|
|
||||||
editedEventId = null,
|
|
||||||
noisy = false,
|
|
||||||
title = "title",
|
|
||||||
description = "description",
|
|
||||||
type = null,
|
|
||||||
timestamp = 0,
|
|
||||||
soundName = null,
|
|
||||||
canBeReplaced = false,
|
|
||||||
isRedacted = false
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessageEvent(
|
|
||||||
eventId = eventId,
|
|
||||||
editedEventId = null,
|
|
||||||
noisy = false,
|
|
||||||
timestamp = 0,
|
|
||||||
senderName = "sender-name",
|
|
||||||
senderId = "sending-id",
|
|
||||||
body = "message-body",
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = "room-name",
|
|
||||||
roomIsDirect = false,
|
|
||||||
canBeReplaced = false,
|
|
||||||
isRedacted = false
|
|
||||||
)
|
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import im.vector.app.test.fixtures.aNotifiableMessageEvent
|
||||||
|
import im.vector.app.test.fixtures.aSimpleNotifiableEvent
|
||||||
|
import im.vector.app.test.fixtures.anInviteNotifiableEvent
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class NotificationEventQueueTest {
|
||||||
|
|
||||||
|
private val seenIdsCache = CircularCache.create<String>(5)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given events when redacting some then marks matching event ids as redacted`() {
|
||||||
|
val queue = givenQueue(listOf(
|
||||||
|
aSimpleNotifiableEvent(eventId = "redacted-id-1"),
|
||||||
|
aNotifiableMessageEvent(eventId = "redacted-id-2"),
|
||||||
|
anInviteNotifiableEvent(eventId = "redacted-id-3"),
|
||||||
|
aSimpleNotifiableEvent(eventId = "kept-id"),
|
||||||
|
))
|
||||||
|
|
||||||
|
queue.markRedacted(listOf("redacted-id-1", "redacted-id-2", "redacted-id-3"))
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(
|
||||||
|
aSimpleNotifiableEvent(eventId = "redacted-id-1", isRedacted = true),
|
||||||
|
aNotifiableMessageEvent(eventId = "redacted-id-2", isRedacted = true),
|
||||||
|
anInviteNotifiableEvent(eventId = "redacted-id-3", isRedacted = true),
|
||||||
|
aSimpleNotifiableEvent(eventId = "kept-id", isRedacted = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given invite event when leaving invited room and syncing then removes event`() {
|
||||||
|
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = "a-room-id")))
|
||||||
|
val roomsLeft = listOf("a-room-id")
|
||||||
|
|
||||||
|
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given invite event when joining invited room and syncing then removes event`() {
|
||||||
|
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = "a-room-id")))
|
||||||
|
val joinedRooms = listOf("a-room-id")
|
||||||
|
|
||||||
|
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = joinedRooms)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given message event when leaving message room and syncing then removes event`() {
|
||||||
|
val queue = givenQueue(listOf(aNotifiableMessageEvent(roomId = "a-room-id")))
|
||||||
|
val roomsLeft = listOf("a-room-id")
|
||||||
|
|
||||||
|
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given events when syncing without rooms left or joined ids then does not change the events`() {
|
||||||
|
val queue = givenQueue(listOf(
|
||||||
|
aNotifiableMessageEvent(roomId = "a-room-id"),
|
||||||
|
anInviteNotifiableEvent(roomId = "a-room-id")
|
||||||
|
))
|
||||||
|
|
||||||
|
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = emptyList())
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(
|
||||||
|
aNotifiableMessageEvent(roomId = "a-room-id"),
|
||||||
|
anInviteNotifiableEvent(roomId = "a-room-id")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given events then is not empty`() {
|
||||||
|
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||||
|
|
||||||
|
queue.isEmpty() shouldBeEqualTo false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no events then is empty`() {
|
||||||
|
val queue = givenQueue(emptyList())
|
||||||
|
|
||||||
|
queue.isEmpty() shouldBeEqualTo true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given events when clearing and adding then removes previous events and adds only new events`() {
|
||||||
|
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||||
|
|
||||||
|
queue.clearAndAdd(listOf(anInviteNotifiableEvent()))
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(anInviteNotifiableEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearing then is empty`() {
|
||||||
|
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||||
|
|
||||||
|
queue.clear()
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no events when adding then adds event`() {
|
||||||
|
val queue = givenQueue(listOf())
|
||||||
|
|
||||||
|
queue.add(aSimpleNotifiableEvent(), seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(aSimpleNotifiableEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no events when adding already seen event then ignores event`() {
|
||||||
|
val queue = givenQueue(listOf())
|
||||||
|
val notifiableEvent = aSimpleNotifiableEvent()
|
||||||
|
seenIdsCache.put(notifiableEvent.eventId)
|
||||||
|
|
||||||
|
queue.add(notifiableEvent, seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given replaceable event when adding event with same id then updates existing event`() {
|
||||||
|
val replaceableEvent = aSimpleNotifiableEvent(canBeReplaced = true)
|
||||||
|
val updatedEvent = replaceableEvent.copy(title = "updated title")
|
||||||
|
val queue = givenQueue(listOf(replaceableEvent))
|
||||||
|
|
||||||
|
queue.add(updatedEvent, seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(updatedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given non replaceable event when adding event with same id then ignores event`() {
|
||||||
|
val nonReplaceableEvent = aSimpleNotifiableEvent(canBeReplaced = false)
|
||||||
|
val updatedEvent = nonReplaceableEvent.copy(title = "updated title")
|
||||||
|
val queue = givenQueue(listOf(nonReplaceableEvent))
|
||||||
|
|
||||||
|
queue.add(updatedEvent, seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(nonReplaceableEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given event when adding new event with edited event id matching the existing event id then updates existing event`() {
|
||||||
|
val editedEvent = aSimpleNotifiableEvent(eventId = "id-to-edit")
|
||||||
|
val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title")
|
||||||
|
val queue = givenQueue(listOf(editedEvent))
|
||||||
|
|
||||||
|
queue.add(updatedEvent, seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(updatedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given event when adding new event with edited event id matching the existing event edited id then updates existing event`() {
|
||||||
|
val editedEvent = aSimpleNotifiableEvent(eventId = "0", editedEventId = "id-to-edit")
|
||||||
|
val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title")
|
||||||
|
val queue = givenQueue(listOf(editedEvent))
|
||||||
|
|
||||||
|
queue.add(updatedEvent, seenEventIds = seenIdsCache)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(updatedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearing membership notification then removes invite events with matching room id`() {
|
||||||
|
val roomId = "a-room-id"
|
||||||
|
val queue = givenQueue(listOf(
|
||||||
|
anInviteNotifiableEvent(roomId = roomId),
|
||||||
|
aNotifiableMessageEvent(roomId = roomId)
|
||||||
|
))
|
||||||
|
|
||||||
|
queue.clearMemberShipNotificationForRoom(roomId)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(aNotifiableMessageEvent(roomId = roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when clearing messages for room then removes message events with matching room id`() {
|
||||||
|
val roomId = "a-room-id"
|
||||||
|
val queue = givenQueue(listOf(
|
||||||
|
anInviteNotifiableEvent(roomId = roomId),
|
||||||
|
aNotifiableMessageEvent(roomId = roomId)
|
||||||
|
))
|
||||||
|
|
||||||
|
queue.clearMessagesForRoom(roomId)
|
||||||
|
|
||||||
|
queue.rawEvents() shouldBeEqualTo listOf(anInviteNotifiableEvent(roomId = roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenQueue(events: List<NotifiableEvent>) = NotificationEventQueue(events.toMutableList())
|
||||||
|
}
|
|
@ -20,6 +20,9 @@ import im.vector.app.features.notifications.ProcessedEvent.Type
|
||||||
import im.vector.app.test.fakes.FakeNotificationUtils
|
import im.vector.app.test.fakes.FakeNotificationUtils
|
||||||
import im.vector.app.test.fakes.FakeRoomGroupMessageCreator
|
import im.vector.app.test.fakes.FakeRoomGroupMessageCreator
|
||||||
import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator
|
import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator
|
||||||
|
import im.vector.app.test.fixtures.aNotifiableMessageEvent
|
||||||
|
import im.vector.app.test.fixtures.aSimpleNotifiableEvent
|
||||||
|
import im.vector.app.test.fixtures.anInviteNotifiableEvent
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue