mirror of
https://github.com/element-hq/element-android
synced 2024-11-25 10:55:38 +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
|
||||
id: result
|
||||
attributes:
|
||||
label: Intended result and actual result
|
||||
label: Outcome
|
||||
placeholder: Tell us what went wrong
|
||||
value: |
|
||||
#### 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
|
||||
attributes:
|
||||
label: Your use case
|
||||
description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
|
||||
description: Please feel welcome to include screenshots or mock ups.
|
||||
placeholder: Tell us what you would like to do!
|
||||
value: |
|
||||
#### What would you like to do?
|
||||
|
|
21
.github/workflows/sanity_test.yml
vendored
21
.github/workflows/sanity_test.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api-level: [28]
|
||||
api-level: [ 29 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
|
@ -56,7 +56,24 @@ jobs:
|
|||
java-version: '11'
|
||||
- name: Run sanity tests on API ${{ matrix.api-level }}
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
continue-on-error: true # allow pipeline to upload failure results
|
||||
with:
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
api-level: ${{ matrix.api-level }}
|
||||
script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest
|
||||
emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||
script: |
|
||||
adb root
|
||||
adb logcat -c
|
||||
touch emulator.log
|
||||
chmod 777 emulator.log
|
||||
adb logcat >> emulator.log &
|
||||
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
|
||||
|
||||
- name: Upload Failing Test Report Log
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sanity-error-results
|
||||
path: |
|
||||
emulator.log
|
||||
failure_screenshots/
|
||||
|
|
2
.github/workflows/triage-incoming.yml
vendored
2
.github/workflows/triage-incoming.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
automate-project-columns:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||
with:
|
||||
project: Issue triage
|
||||
column: Incoming
|
||||
|
|
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -141,7 +142,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
@SuppressLint("WrongConstant")
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
}
|
||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||
|
@ -347,7 +353,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
@SuppressLint("WrongConstant")
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
}
|
||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||
|
|
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 = [
|
||||
|
||||
'minSdk' : 21,
|
||||
'compileSdk' : 30,
|
||||
'targetSdk' : 30,
|
||||
'compileSdk' : 31,
|
||||
'targetSdk' : 31,
|
||||
'sourceCompat' : JavaVersion.VERSION_11,
|
||||
'targetCompat' : JavaVersion.VERSION_11,
|
||||
]
|
||||
|
@ -11,12 +11,12 @@ def gradle = "7.0.3"
|
|||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.5.31"
|
||||
def kotlinCoroutines = "1.5.2"
|
||||
def dagger = "2.40"
|
||||
def dagger = "2.40.1"
|
||||
def retrofit = "2.9.0"
|
||||
def arrow = "0.8.2"
|
||||
def markwon = "4.6.2"
|
||||
def moshi = "1.12.0"
|
||||
def lifecycle = "2.2.0"
|
||||
def lifecycle = "2.4.0"
|
||||
def flowBinding = "1.2.0"
|
||||
def epoxy = "4.6.2"
|
||||
def mavericks = "2.4.0"
|
||||
|
@ -46,18 +46,18 @@ ext.libs = [
|
|||
],
|
||||
androidx : [
|
||||
'appCompat' : "androidx.appcompat:appcompat:1.3.1",
|
||||
'core' : "androidx.core:core-ktx:1.6.0",
|
||||
'core' : "androidx.core:core-ktx:1.7.0",
|
||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6",
|
||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1",
|
||||
'work' : "androidx.work:work-runtime-ktx:2.6.0",
|
||||
'work' : "androidx.work:work-runtime-ktx:2.7.0",
|
||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
||||
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
|
||||
'junit' : "androidx.test.ext:junit:1.1.3",
|
||||
'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle",
|
||||
'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle",
|
||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1",
|
||||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionSha256Sum=00b273629df4ce46e68df232161d5a7c4e495b9a029ce6e0420f071e21316867
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -129,9 +129,9 @@
|
|||
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
|
||||
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
|
||||
|
||||
<attr name="vctr_voice_message_toast_background" format="color" />
|
||||
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color>
|
||||
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color>
|
||||
<attr name="vctr_toast_background" format="color" />
|
||||
<color name="vctr_toast_background_light">@color/palette_black_900</color>
|
||||
<color name="vctr_toast_background_dark">@color/palette_gray_400</color>
|
||||
|
||||
<!-- Presence Indicator colors -->
|
||||
<attr name="vctr_presence_indicator_offline" format="color" />
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
<item name="lineHeight">24sp</item>
|
||||
</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">
|
||||
<item name="android:minWidth">94dp</item>
|
||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
<?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.EditText.Composer" parent="Widget.AppCompat.EditText">
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
<item name="android:inputType">textCapSentences|textMultiLine</item>
|
||||
|
@ -19,9 +11,4 @@
|
|||
<item name="android:textColor">?vctr_message_text_color</item>
|
||||
</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>
|
|
@ -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>
|
||||
</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>
|
|
@ -140,8 +140,7 @@
|
|||
<!-- Keywords -->
|
||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
|
||||
<item name="vctr_toast_background">@color/vctr_toast_background_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||
|
|
|
@ -143,8 +143,7 @@
|
|||
<!-- Keywords -->
|
||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
|
||||
<item name="vctr_toast_background">@color/vctr_toast_background_light</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||
|
|
|
@ -44,6 +44,7 @@ android {
|
|||
}
|
||||
|
||||
testOptions {
|
||||
// Comment to run on Android 12
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
|
@ -106,8 +107,9 @@ dependencies {
|
|||
implementation libs.androidx.appCompat
|
||||
implementation libs.androidx.core
|
||||
|
||||
implementation libs.androidx.lifecycleExtensions
|
||||
implementation libs.androidx.lifecycleJava8
|
||||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleCommon
|
||||
implementation libs.androidx.lifecycleProcess
|
||||
|
||||
// Network
|
||||
implementation libs.squareup.retrofit
|
||||
|
@ -156,10 +158,10 @@ dependencies {
|
|||
implementation libs.apache.commonsImaging
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.7'
|
||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
testImplementation libs.mockk.mockk
|
||||
|
|
|
@ -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?
|
||||
|
||||
interface PushRuleListener {
|
||||
fun onMatchRule(event: Event, actions: List<Action>)
|
||||
fun onRoomJoined(roomId: String)
|
||||
fun onRoomLeft(roomId: String)
|
||||
fun onEventRedacted(redactedEventId: String)
|
||||
fun batchFinish()
|
||||
fun onEvents(pushEvents: PushEvents)
|
||||
}
|
||||
|
||||
fun getKeywords(): LiveData<Set<String>>
|
||||
|
|
|
@ -38,14 +38,22 @@ data class MXKey(
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
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>
|
||||
* "signed_curve25519:AAAAFw": {
|
||||
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
|
||||
* "fallback" : true|false
|
||||
* "signatures": {
|
||||
* "@userId:matrix.org": {
|
||||
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
|
||||
|
@ -107,7 +116,8 @@ data class MXKey(
|
|||
type = components[0],
|
||||
keyId = components[1],
|
||||
value = params["key"] as String,
|
||||
signatures = params["signatures"] as Map<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 com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||
|
@ -142,79 +143,6 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
||||
}
|
||||
|
||||
// fun processEvents(events: List<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>> {
|
||||
// Keywords are all content rules that don't start with '.'
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
|
@ -229,4 +157,16 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||
results.firstOrNull().orEmpty().toSet()
|
||||
}
|
||||
}
|
||||
|
||||
fun dispatchEvents(pushEvents: PushEvents) {
|
||||
synchronized(listeners) {
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.onEvents(pushEvents)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Error while dispatching push events")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.notification
|
||||
|
||||
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.isInvitation
|
||||
|
@ -39,14 +40,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
) : ProcessEventForPushTask {
|
||||
|
||||
override suspend fun execute(params: ProcessEventForPushTask.Params) {
|
||||
// Handle left rooms
|
||||
params.syncResponse.leave.keys.forEach {
|
||||
defaultPushRuleService.dispatchRoomLeft(it)
|
||||
}
|
||||
// Handle joined rooms
|
||||
params.syncResponse.join.keys.forEach {
|
||||
defaultPushRuleService.dispatchRoomJoined(it)
|
||||
}
|
||||
val newJoinEvents = params.syncResponse.join
|
||||
.mapNotNull { (key, value) ->
|
||||
value.timeline?.events?.mapNotNull {
|
||||
|
@ -74,10 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
}
|
||||
Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
|
||||
" to check for push rules with ${params.rules.size} rules")
|
||||
allEvents.forEach { event ->
|
||||
val matchedEvents = allEvents.mapNotNull { event ->
|
||||
pushRuleFinder.fulfilledBingRule(event, params.rules)?.let {
|
||||
Timber.v("[PushRules] Rule $it match for event ${event.eventId}")
|
||||
defaultPushRuleService.dispatchBing(event, it)
|
||||
event to it
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,10 +84,13 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
|
||||
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
|
||||
|
||||
allRedactedEvents.forEach { redactedEventId ->
|
||||
defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
|
||||
}
|
||||
|
||||
defaultPushRuleService.dispatchFinish()
|
||||
defaultPushRuleService.dispatchEvents(
|
||||
PushEvents(
|
||||
matchedEvents = matchedEvents,
|
||||
roomsJoined = params.syncResponse.join.keys,
|
||||
roomsLeft = params.syncResponse.leave.keys,
|
||||
redactedEventIds = allRedactedEvents
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.util
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.matrix.android.sdk.internal.di.MatrixScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -27,13 +26,12 @@ import javax.inject.Inject
|
|||
* To be attached to ProcessLifecycleOwner lifecycle
|
||||
*/
|
||||
@MatrixScope
|
||||
internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
|
||||
internal class BackgroundDetectionObserver @Inject constructor() : DefaultLifecycleObserver {
|
||||
|
||||
var isInBackground: Boolean = true
|
||||
private set
|
||||
|
||||
private
|
||||
val listeners = LinkedHashSet<Listener>()
|
||||
private val listeners = LinkedHashSet<Listener>()
|
||||
|
||||
fun register(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
|
@ -43,15 +41,13 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
|
|||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun onMoveToForeground() {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.v("App returning to foreground…")
|
||||
isInBackground = false
|
||||
listeners.forEach { it.onMoveToForeground() }
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
fun onMoveToBackground() {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
Timber.v("App going to background…")
|
||||
isInBackground = true
|
||||
listeners.forEach { it.onMoveToBackground() }
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.provider.ContactsContract
|
||||
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
|
||||
/**
|
||||
* Contact Picker implementation
|
||||
|
@ -49,9 +50,9 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
|||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
|
||||
val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
|
||||
val photoUriColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI)
|
||||
val idColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts._ID) ?: return@use
|
||||
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
|
||||
|
||||
val contactId = cursor.getInt(idColumn)
|
||||
var name = cursor.getString(nameColumn)
|
||||
|
@ -72,10 +73,13 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
|||
selection,
|
||||
selectionArgs,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
|
||||
val contactData = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
|
||||
)?.use inner@{ innerCursor ->
|
||||
val mimeTypeColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.MIMETYPE) ?: return@inner
|
||||
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
|
||||
|
||||
while (innerCursor.moveToNext()) {
|
||||
val mimeType = innerCursor.getString(mimeTypeColumnIndex)
|
||||
val contactData = innerCursor.getString(data1ColumnIndex)
|
||||
|
||||
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||
name = contactData
|
||||
|
@ -115,7 +119,10 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
|||
selectionArgs,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
return if (cursor.moveToFirst()) cursor.getInt(cursor.getColumnIndex(ContactsContract.RawContacts._ID)) else null
|
||||
return if (cursor.moveToFirst()) {
|
||||
cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
|
||||
?.let { cursor.getInt(it) }
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Intent
|
|||
import android.provider.OpenableColumns
|
||||
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
||||
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
import im.vector.lib.multipicker.utils.isMimeTypeAudio
|
||||
import im.vector.lib.multipicker.utils.isMimeTypeImage
|
||||
import im.vector.lib.multipicker.utils.isMimeTypeVideo
|
||||
|
@ -49,8 +50,8 @@ class FilePicker : Picker<MultiPickerBaseType>() {
|
|||
// Other files
|
||||
context.contentResolver.query(selectedUri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
|
||||
if (cursor.moveToFirst()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
|
|
|
@ -37,8 +37,8 @@ internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType?
|
|||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
||||
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.DISPLAY_NAME) ?: return@use null
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
|
@ -75,8 +75,8 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
|
|||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE)
|
||||
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.DISPLAY_NAME) ?: return@use null
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
|
@ -124,8 +124,8 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
|||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
|
||||
val nameColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.DISPLAY_NAME) ?: return@use null
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
|
|
|
@ -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",
|
||||
"hand",
|
||||
"hands",
|
||||
"gesture"
|
||||
"gesture",
|
||||
"shaka"
|
||||
]
|
||||
},
|
||||
"backhand-index-pointing-left": {
|
||||
|
|
|
@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1
|
|||
PARAM_APK=$2
|
||||
|
||||
# Other params
|
||||
BUILD_TOOLS_VERSION="30.0.3"
|
||||
BUILD_TOOLS_VERSION="31.0.0-rc5"
|
||||
MIN_SDK_VERSION=21
|
||||
|
||||
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
||||
|
|
|
@ -23,7 +23,7 @@ PARAM_KS_PASS=$3
|
|||
PARAM_KEY_PASS=$4
|
||||
|
||||
# Other params
|
||||
BUILD_TOOLS_VERSION="30.0.3"
|
||||
BUILD_TOOLS_VERSION="31.0.0-rc5"
|
||||
MIN_SDK_VERSION=21
|
||||
|
||||
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."
|
||||
|
|
|
@ -210,6 +210,7 @@ android {
|
|||
// This property does not affect tests that you run using Android Studio.”
|
||||
animationsDisabled = true
|
||||
|
||||
// Comment to run on Android 12
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
|
@ -356,8 +357,10 @@ dependencies {
|
|||
|
||||
implementation libs.squareup.moshi
|
||||
kapt libs.squareup.moshiKotlin
|
||||
implementation libs.androidx.lifecycleExtensions
|
||||
|
||||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleLivedata
|
||||
implementation libs.androidx.lifecycleProcess
|
||||
|
||||
implementation libs.androidx.datastore
|
||||
implementation libs.androidx.datastorepreferences
|
||||
|
@ -370,7 +373,7 @@ dependencies {
|
|||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
|
||||
|
||||
// FlowBinding
|
||||
implementation libs.github.flowBinding
|
||||
|
@ -411,7 +414,7 @@ dependencies {
|
|||
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
||||
|
||||
// Custom Tab
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
|
||||
// Passphrase strength helper
|
||||
implementation 'com.nulab-inc:zxcvbn:1.5.2'
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app
|
|||
import android.app.Activity
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
|
@ -35,6 +36,11 @@ import androidx.test.runner.lifecycle.ActivityLifecycleCallback
|
|||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
|
||||
import androidx.test.runner.lifecycle.Stage
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.StringDescription
|
||||
|
@ -52,6 +58,18 @@ object EspressoHelper {
|
|||
}
|
||||
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 {
|
||||
|
@ -216,3 +234,46 @@ fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) {
|
|||
block()
|
||||
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.runners.AndroidJUnit4
|
||||
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.getString
|
||||
import im.vector.app.ui.robot.ElementRobot
|
||||
import im.vector.app.ui.robot.withDeveloperMode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.UUID
|
||||
|
||||
|
@ -34,7 +39,9 @@ import java.util.UUID
|
|||
class UiAllScreensSanityTest {
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
val testRule = RuleChain
|
||||
.outerRule(ActivityScenarioRule(MainActivity::class.java))
|
||||
.around(ScreenshotFailureRule())
|
||||
|
||||
private val elementRobot = ElementRobot()
|
||||
|
||||
|
@ -69,13 +76,30 @@ class UiAllScreensSanityTest {
|
|||
createNewRoom {
|
||||
crawl()
|
||||
createRoom {
|
||||
postMessage("Hello world!")
|
||||
val message = "Hello world!"
|
||||
postMessage(message)
|
||||
crawl()
|
||||
crawlMessage(message)
|
||||
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 {
|
||||
verifyCreatedRoom()
|
||||
}
|
||||
|
|
|
@ -141,3 +141,9 @@ class ElementRobot {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
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.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn
|
||||
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
|
||||
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
|
||||
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
|
||||
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
|
||||
import im.vector.app.R
|
||||
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||
import im.vector.app.interactWithSheet
|
||||
import im.vector.app.waitForView
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
|
@ -39,7 +42,9 @@ class RoomDetailRobot {
|
|||
|
||||
fun postMessage(content: String) {
|
||||
writeTo(R.id.composerEditText, content)
|
||||
waitUntilViewVisible(withId(R.id.sendButton))
|
||||
clickOn(R.id.sendButton)
|
||||
waitUntilViewVisible(withText(content))
|
||||
}
|
||||
|
||||
fun crawl() {
|
||||
|
@ -55,61 +60,54 @@ class RoomDetailRobot {
|
|||
pressBack()
|
||||
clickMenu(R.id.search)
|
||||
pressBack()
|
||||
// Long click on the message
|
||||
longClickOnMessageTest()
|
||||
}
|
||||
|
||||
private fun longClickOnMessageTest() {
|
||||
fun crawlMessage(message: String) {
|
||||
// Test quick reaction
|
||||
longClickOnMessage()
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
||||
// Add quick reaction
|
||||
clickOn("\uD83D\uDC4D️") // 👍
|
||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
||||
|
||||
val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
|
||||
openMessageMenu(message) {
|
||||
addQuickReaction(quickReaction)
|
||||
}
|
||||
// Open reactions
|
||||
longClickOn("\uD83D\uDC4D️") // 👍
|
||||
longClickOn(quickReaction)
|
||||
// wait for bottom sheet
|
||||
pressBack()
|
||||
|
||||
// Test add reaction
|
||||
longClickOnMessage()
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
||||
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))
|
||||
|
||||
openMessageMenu(message) {
|
||||
addReactionFromEmojiPicker()
|
||||
}
|
||||
// Test Edit mode
|
||||
longClickOnMessage()
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
||||
clickOn(R.string.edit)
|
||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
||||
openMessageMenu(message) {
|
||||
edit()
|
||||
}
|
||||
// TODO Cancel action
|
||||
writeTo(R.id.composerEditText, "Hello universe!")
|
||||
// Wait a bit for the keyboard layout to update
|
||||
sleep(30)
|
||||
waitUntilViewVisible(withId(R.id.sendButton))
|
||||
clickOn(R.id.sendButton)
|
||||
// Wait for the UI to update
|
||||
sleep(1000)
|
||||
waitUntilViewVisible(withText("Hello universe! (edited)"))
|
||||
// Open edit history
|
||||
longClickOnMessage("Hello universe! (edited)")
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView))
|
||||
clickOn(R.string.message_view_edit_history)
|
||||
pressBack()
|
||||
openMessageMenu("Hello universe! (edited)") {
|
||||
editHistory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun longClickOnMessage(text: String = "Hello world!") {
|
||||
Espresso.onView(withId(R.id.timelineRecyclerView))
|
||||
fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) {
|
||||
onView(withId(R.id.timelineRecyclerView))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(text)),
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
|
||||
ViewActions.longClick()
|
||||
)
|
||||
)
|
||||
interactWithSheet<MessageActionsBottomSheet>(contentMatcher = withId(R.id.bottomSheetRecyclerView)) {
|
||||
val messageMenuRobot = MessageMenuRobot()
|
||||
block(messageMenuRobot)
|
||||
if (!messageMenuRobot.autoClosed) {
|
||||
pressBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSettings(block: RoomSettingsRobot.() -> Unit) {
|
||||
|
|
|
@ -17,38 +17,46 @@
|
|||
package im.vector.app.ui.robot
|
||||
|
||||
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.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||
import im.vector.app.R
|
||||
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
|
||||
|
||||
class RoomListRobot {
|
||||
|
||||
fun openRoom(roomName: String, block: RoomDetailRobot.() -> Unit) {
|
||||
clickOn(roomName)
|
||||
block(RoomDetailRobot())
|
||||
pressBack()
|
||||
}
|
||||
|
||||
fun verifyCreatedRoom() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.roomListView))
|
||||
onView(ViewMatchers.withId(R.id.roomListView))
|
||||
.perform(
|
||||
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()
|
||||
)
|
||||
)
|
||||
Espresso.pressBack()
|
||||
pressBack()
|
||||
}
|
||||
|
||||
fun newRoom(block: NewRoomRobot.() -> Unit) {
|
||||
BaristaClickInteractions.clickOn(R.id.createGroupRoomButton)
|
||||
clickOn(R.id.createGroupRoomButton)
|
||||
waitUntilActivityVisible<RoomDirectoryActivity> {
|
||||
BaristaVisibilityAssertions.assertDisplayed(R.id.publicRoomsList)
|
||||
}
|
||||
val newRoomRobot = NewRoomRobot()
|
||||
block(newRoomRobot)
|
||||
if (!newRoomRobot.createdRoom) {
|
||||
Espresso.pressBack()
|
||||
pressBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,11 @@
|
|||
package im.vector.app.ui.robot.settings
|
||||
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||
import im.vector.app.R
|
||||
import im.vector.app.espresso.tools.clickOnPreference
|
||||
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||
|
||||
class SettingsAdvancedRobot {
|
||||
|
||||
|
@ -28,20 +31,19 @@ class SettingsAdvancedRobot {
|
|||
|
||||
clickOnPreference(R.string.settings_push_rules)
|
||||
pressBack()
|
||||
}
|
||||
|
||||
/* TODO P2 test developer screens
|
||||
// Enable developer mode
|
||||
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
|
||||
fun toggleDeveloperMode() {
|
||||
clickOn(R.string.settings_developer_mode_summary)
|
||||
}
|
||||
|
||||
clickOnPreference(R.string.settings_account_data)
|
||||
clickOn("m.push_rules")
|
||||
pressBack()
|
||||
pressBack()
|
||||
clickOnPreference(R.string.settings_key_requests)
|
||||
pressBack()
|
||||
|
||||
// Disable developer mode
|
||||
clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY")
|
||||
*/
|
||||
fun crawlDeveloperOptions() {
|
||||
clickOnPreference(R.string.settings_account_data)
|
||||
waitUntilViewVisible(withText("m.push_rules"))
|
||||
clickOn("m.push_rules")
|
||||
pressBack()
|
||||
pressBack()
|
||||
clickOnPreference(R.string.settings_key_requests)
|
||||
pressBack()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@ import im.vector.app.clickOnAndGoBack
|
|||
|
||||
class SettingsRobot {
|
||||
|
||||
fun toggleDeveloperMode() {
|
||||
advancedSettings {
|
||||
toggleDeveloperMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun general(block: SettingsGeneralRobot.() -> Unit) {
|
||||
clickOnAndGoBack(R.string.settings_general_title) { block(SettingsGeneralRobot()) }
|
||||
}
|
||||
|
@ -50,7 +56,9 @@ class SettingsRobot {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
|
||||
<application>
|
||||
|
||||
<receiver android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver">
|
||||
<receiver
|
||||
android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<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.getSystemService
|
||||
import im.vector.app.core.extensions.singletonEntryPoint
|
||||
import im.vector.app.core.platform.PendingIntentCompat
|
||||
import im.vector.app.core.services.VectorSyncService
|
||||
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
||||
import timber.log.Timber
|
||||
|
@ -67,7 +68,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
|
|||
putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
|
||||
putExtra(SyncService.EXTRA_PERIODIC, true)
|
||||
}
|
||||
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val pIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
REQUEST_CODE,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L
|
||||
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
|
@ -80,7 +86,12 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
|
|||
fun cancelAlarm(context: Context) {
|
||||
Timber.v("## Sync: Cancel alarm for background sync")
|
||||
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
|
||||
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val pIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
REQUEST_CODE,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
val alarmMgr = context.getSystemService<AlarmManager>()!!
|
||||
alarmMgr.cancel(pIntent)
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<service android:name=".gplay.push.fcm.VectorFirebaseMessagingService">
|
||||
<service
|
||||
android:name=".gplay.push.fcm.VectorFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
|
|
|
@ -201,8 +201,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
resolvedEvent
|
||||
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
|
||||
?.let {
|
||||
notificationDrawerManager.onNotifiableEventReceived(it)
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
package="im.vector.app">
|
||||
|
||||
<!-- 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.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -418,6 +421,22 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/sdk_provider_paths" />
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package im.vector.app
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import arrow.core.Option
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.utils.BehaviorDataSource
|
||||
|
@ -57,7 +56,7 @@ class AppStateHandler @Inject constructor(
|
|||
private val sessionDataSource: ActiveSessionDataSource,
|
||||
private val uiStateRepository: UiStateRepository,
|
||||
private val activeSessionHolder: ActiveSessionHolder
|
||||
) : LifecycleObserver {
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
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
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
observeActiveSession()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
coroutineScope.coroutineContext.cancelChildren()
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) {
|
||||
|
|
|
@ -27,9 +27,8 @@ import android.os.HandlerThread
|
|||
import android.os.StrictMode
|
||||
import androidx.core.provider.FontRequest
|
||||
import androidx.core.provider.FontsContractCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDex
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
|
@ -166,9 +165,8 @@ class VectorApplication :
|
|||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(startSyncOnFirstStart)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
Timber.i("App entered foreground")
|
||||
FcmHelper.onEnterForeground(appContext, activeSessionHolder)
|
||||
activeSessionHolder.getSafeActiveSession()?.also {
|
||||
|
@ -176,8 +174,7 @@ class VectorApplication :
|
|||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Timber.i("App entered background") // call persistInfo
|
||||
notificationDrawerManager.persistInfo()
|
||||
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
||||
|
@ -198,9 +195,8 @@ class VectorApplication :
|
|||
EmojiManager.install(GoogleEmojiProvider())
|
||||
}
|
||||
|
||||
private val startSyncOnFirstStart = object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun onStart() {
|
||||
private val startSyncOnFirstStart = object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.i("App process started")
|
||||
authenticationService.getLastAuthenticatedSession()?.startSyncing(appContext)
|
||||
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
package im.vector.app.core.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import androidx.annotation.WorkerThread
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
@ -57,16 +57,20 @@ class ContactsDataSource @Inject constructor(
|
|||
)
|
||||
?.use { cursor ->
|
||||
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()) {
|
||||
val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
|
||||
val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
|
||||
val id = cursor.getLong(idColumnIndex)
|
||||
val displayName = cursor.getString(displayNameColumnIndex)
|
||||
|
||||
val mappedContactBuilder = MappedContactBuilder(
|
||||
id = id,
|
||||
displayName = displayName
|
||||
)
|
||||
|
||||
cursor.getString(ContactsContract.Data.PHOTO_URI)
|
||||
photoUriColumnIndex
|
||||
?.let { cursor.getString(it) }
|
||||
?.let { Uri.parse(it) }
|
||||
?.let { mappedContactBuilder.photoURI = it }
|
||||
|
||||
|
@ -85,12 +89,15 @@ class ContactsDataSource @Inject constructor(
|
|||
null,
|
||||
null,
|
||||
null)
|
||||
?.use { innerCursor ->
|
||||
while (innerCursor.moveToNext()) {
|
||||
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
|
||||
?.let { map[it] }
|
||||
?.use { cursor ->
|
||||
val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.CONTACT_ID) ?: return@use
|
||||
val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||
.let { map[it] }
|
||||
?: continue
|
||||
innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
cursor.getString(phoneNumberColumnIndex)
|
||||
?.let {
|
||||
mappedContactBuilder.msisdns.add(
|
||||
MappedMsisdn(
|
||||
|
@ -114,14 +121,17 @@ class ContactsDataSource @Inject constructor(
|
|||
null,
|
||||
null,
|
||||
null)
|
||||
?.use { innerCursor ->
|
||||
while (innerCursor.moveToNext()) {
|
||||
?.use { cursor ->
|
||||
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
|
||||
// if the email addresses were stored in an array
|
||||
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
|
||||
?.let { map[it] }
|
||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||
.let { map[it] }
|
||||
?: continue
|
||||
innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
|
||||
cursor.getString(emailColumnIndex)
|
||||
?.let {
|
||||
mappedContactBuilder.emails.add(
|
||||
MappedEmail(
|
||||
|
@ -140,16 +150,4 @@ class ContactsDataSource @Inject constructor(
|
|||
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
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.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
|
||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||
if (context != null && uri.scheme == "content") {
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
return it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
context.contentResolver.query(uri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME)
|
||||
?.let { cursor.getString(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return uri.path?.substringAfterLast('/')
|
||||
}
|
||||
|
|
|
@ -18,58 +18,56 @@ package im.vector.app.core.platform
|
|||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
|
||||
fun <T> LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy<T> = LifecycleAwareLazy(this, initializer)
|
||||
|
||||
private object UninitializedValue
|
||||
|
||||
class LifecycleAwareLazy<out T>(
|
||||
private val owner: LifecycleOwner,
|
||||
initializer: () -> T
|
||||
) : Lazy<T>, LifecycleObserver {
|
||||
private val owner: LifecycleOwner,
|
||||
initializer: () -> T
|
||||
) : 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")
|
||||
override val value: T
|
||||
@MainThread
|
||||
get() {
|
||||
if (_value === UninitializedValue) {
|
||||
_value = initializer!!()
|
||||
attachToLifecycle()
|
||||
}
|
||||
return _value as T
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override val value: T
|
||||
@MainThread
|
||||
get() {
|
||||
if (_value === UninitializedValue) {
|
||||
_value = initializer!!()
|
||||
attachToLifecycle()
|
||||
}
|
||||
return _value as T
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
_value = UninitializedValue
|
||||
detachFromLifecycle()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun resetValue() {
|
||||
_value = UninitializedValue
|
||||
detachFromLifecycle()
|
||||
}
|
||||
|
||||
private fun attachToLifecycle() {
|
||||
if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
throw IllegalStateException("Initialization failed because lifecycle has been destroyed!")
|
||||
private fun attachToLifecycle() {
|
||||
if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
throw IllegalStateException("Initialization failed because lifecycle has been destroyed!")
|
||||
}
|
||||
getLifecycleOwner().lifecycle.addObserver(this)
|
||||
}
|
||||
getLifecycleOwner().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
private fun detachFromLifecycle() {
|
||||
getLifecycleOwner().lifecycle.removeObserver(this)
|
||||
}
|
||||
private fun detachFromLifecycle() {
|
||||
getLifecycleOwner().lifecycle.removeObserver(this)
|
||||
}
|
||||
|
||||
private fun getLifecycleOwner() = when (owner) {
|
||||
is Fragment -> owner.viewLifecycleOwner
|
||||
else -> owner
|
||||
}
|
||||
private fun getLifecycleOwner() = when (owner) {
|
||||
is Fragment -> owner.viewLifecycleOwner
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,17 +14,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.telecom
|
||||
package im.vector.app.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.content.getSystemService
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
|
||||
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 telephonyManager = context.getSystemService<TelephonyManager>()
|
||||
?: return false
|
||||
return telephonyManager.callState != TelephonyManager.CALL_STATE_IDLE
|
||||
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.core.platform
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
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
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
@SuppressLint("WrongConstant")
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
}
|
||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||
|
|
|
@ -32,6 +32,7 @@ import androidx.work.Worker
|
|||
import androidx.work.WorkerParameters
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.PendingIntentCompat
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.settings.BackgroundSyncMode
|
||||
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
||||
|
@ -199,9 +200,9 @@ private fun Context.rescheduleSyncService(sessionId: String,
|
|||
startService(intent)
|
||||
} else {
|
||||
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 {
|
||||
PendingIntent.getService(this, 0, intent, 0)
|
||||
PendingIntent.getService(this, 0, intent, PendingIntentCompat.FLAG_IMMUTABLE)
|
||||
}
|
||||
val firstMillis = System.currentTimeMillis() + syncDelaySeconds * 1000L
|
||||
val alarmMgr = getSystemService<AlarmManager>()!!
|
||||
|
|
|
@ -20,9 +20,8 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
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() {
|
||||
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,
|
||||
private val onBroadcastEvent: (ConferenceEvent) -> Unit) :
|
||||
LifecycleObserver {
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
// See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
|
||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||
|
@ -61,8 +60,7 @@ class ConferenceEventObserver(private val context: Context,
|
|||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun unregisterForBroadcastMessages() {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
try {
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
|
||||
} catch (throwable: Throwable) {
|
||||
|
@ -70,8 +68,7 @@ class ConferenceEventObserver(private val context: Context,
|
|||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
fun registerForBroadcastMessages() {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
val intentFilter = IntentFilter()
|
||||
for (type in BroadcastEvent.Type.values()) {
|
||||
intentFilter.addAction(type.action)
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.services.CallService
|
||||
|
@ -70,7 +69,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
|
|||
class WebRtcCallManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) : CallListener, LifecycleObserver {
|
||||
) : CallListener,
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
private val currentSession: Session?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()
|
||||
|
@ -133,13 +133,11 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
private var isInBackground: Boolean = true
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
isInBackground = false
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
isInBackground = true
|
||||
}
|
||||
|
||||
|
|
|
@ -29,13 +29,8 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
|
|||
private val stringProvider: StringProvider
|
||||
) : ViewModel() {
|
||||
|
||||
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
||||
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
||||
|
||||
init {
|
||||
recoveryCode.value = null
|
||||
recoveryCodeErrorText.value = null
|
||||
}
|
||||
var recoveryCode: MutableLiveData<String?> = MutableLiveData(null)
|
||||
var recoveryCodeErrorText: MutableLiveData<String?> = MutableLiveData(null)
|
||||
|
||||
// ========= Actions =========
|
||||
fun updateCode(newValue: String) {
|
||||
|
|
|
@ -28,13 +28,8 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
|
|||
private val stringProvider: StringProvider
|
||||
) : ViewModel() {
|
||||
|
||||
var passphrase: MutableLiveData<String> = MutableLiveData()
|
||||
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
||||
|
||||
init {
|
||||
passphrase.value = null
|
||||
passphraseErrorText.value = null
|
||||
}
|
||||
var passphrase: MutableLiveData<String?> = MutableLiveData(null)
|
||||
var passphraseErrorText: MutableLiveData<String?> = MutableLiveData(null)
|
||||
|
||||
// ========= Actions =========
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
|
|||
|
||||
lateinit var session: Session
|
||||
|
||||
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
||||
var keyVersionResult: MutableLiveData<KeysVersionResult?> = MutableLiveData(null)
|
||||
|
||||
var keySourceModel: MutableLiveData<KeySource> = MutableLiveData()
|
||||
|
||||
|
@ -69,17 +69,11 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
|
|||
val navigateEvent: LiveData<LiveEvent<String>>
|
||||
get() = _navigateEvent
|
||||
|
||||
var loadingEvent: MutableLiveData<WaitingViewData> = MutableLiveData()
|
||||
var loadingEvent: MutableLiveData<WaitingViewData?> = MutableLiveData(null)
|
||||
|
||||
var importKeyResult: ImportRoomKeysResult? = null
|
||||
var importRoomKeysFinishWithResult: MutableLiveData<LiveEvent<ImportRoomKeysResult>> = MutableLiveData()
|
||||
|
||||
init {
|
||||
keyVersionResult.value = null
|
||||
_keyVersionResultError.value = null
|
||||
loadingEvent.value = null
|
||||
}
|
||||
|
||||
fun initSession(session: Session) {
|
||||
this.session = session
|
||||
}
|
||||
|
|
|
@ -68,23 +68,15 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
|
|||
// Step 3
|
||||
// Var to ignore events from previous request(s) to generate a recovery key
|
||||
private var currentRequestId: MutableLiveData<Long> = MutableLiveData()
|
||||
var recoveryKey: MutableLiveData<String> = MutableLiveData()
|
||||
var prepareRecoverFailError: MutableLiveData<Throwable> = MutableLiveData()
|
||||
var recoveryKey: MutableLiveData<String?> = MutableLiveData(null)
|
||||
var prepareRecoverFailError: MutableLiveData<Throwable?> = MutableLiveData(null)
|
||||
var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
|
||||
var copyHasBeenMade = false
|
||||
var isCreatingBackupVersion: MutableLiveData<Boolean> = MutableLiveData()
|
||||
var creatingBackupError: MutableLiveData<Throwable> = MutableLiveData()
|
||||
var isCreatingBackupVersion: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
var creatingBackupError: MutableLiveData<Throwable?> = MutableLiveData(null)
|
||||
var keysVersion: MutableLiveData<KeysVersion> = MutableLiveData()
|
||||
|
||||
var loadingStatus: MutableLiveData<WaitingViewData> = MutableLiveData()
|
||||
|
||||
init {
|
||||
recoveryKey.value = null
|
||||
isCreatingBackupVersion.value = false
|
||||
prepareRecoverFailError.value = null
|
||||
creatingBackupError.value = null
|
||||
loadingStatus.value = null
|
||||
}
|
||||
var loadingStatus: MutableLiveData<WaitingViewData?> = MutableLiveData(null)
|
||||
|
||||
fun initSession(session: Session) {
|
||||
this.session = session
|
||||
|
|
|
@ -21,8 +21,10 @@ import android.content.pm.ShortcutManager
|
|||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
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.PinCodeStoreListener
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -43,6 +45,7 @@ import javax.inject.Inject
|
|||
|
||||
class ShortcutsHandler @Inject constructor(
|
||||
private val context: Context,
|
||||
private val stringProvider: StringProvider,
|
||||
private val appDispatchers: CoroutineDispatchers,
|
||||
private val shortcutCreator: ShortcutCreator,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
|
@ -72,7 +75,7 @@ class ShortcutsHandler @Inject constructor(
|
|||
.onCompletion { pinCodeStore.removeListener(this@ShortcutsHandler) }
|
||||
.onEach { rooms ->
|
||||
// Remove dead shortcuts (i.e. deleted rooms)
|
||||
removeDeadShortcut(rooms.map { it.roomId })
|
||||
removeDeadShortcuts(rooms.map { it.roomId })
|
||||
|
||||
// Create shortcuts
|
||||
createShortcuts(rooms)
|
||||
|
@ -81,7 +84,7 @@ class ShortcutsHandler @Inject constructor(
|
|||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun removeDeadShortcut(roomIds: List<String>) {
|
||||
private fun removeDeadShortcuts(roomIds: List<String>) {
|
||||
val deadShortcutIds = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC)
|
||||
.map { it.id }
|
||||
.filter { !roomIds.contains(it) }
|
||||
|
@ -91,7 +94,11 @@ class ShortcutsHandler @Inject constructor(
|
|||
ShortcutManagerCompat.removeLongLivedShortcuts(context, deadShortcutIds)
|
||||
if (isRequestPinShortcutSupported) {
|
||||
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
context.getSystemService<ShortcutManager>()
|
||||
?.let {
|
||||
it.disableShortcuts(it.pinnedShortcuts.map { pinnedShortcut -> pinnedShortcut.id })
|
||||
?.pinnedShortcuts
|
||||
?.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
|
||||
|
||||
override fun onAcceptInvite() {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) }
|
||||
roomDetailViewModel.handle(RoomDetailAction.AcceptInvite)
|
||||
}
|
||||
|
||||
override fun onRejectInvite() {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) }
|
||||
roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,15 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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.InputConnectionCompat
|
||||
import com.vanniktech.emoji.EmojiEditText
|
||||
|
@ -33,7 +35,7 @@ import im.vector.app.features.html.PillImageSpan
|
|||
import timber.log.Timber
|
||||
|
||||
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 {
|
||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||
|
@ -43,23 +45,35 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||
var callback: Callback? = null
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
|
||||
val ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
|
||||
var ic = super.onCreateInputConnection(editorInfo) ?: return null
|
||||
val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this) ?: arrayOf("image/*")
|
||||
|
||||
val callback =
|
||||
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
|
||||
val lacksPermission = (flags and
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
return@OnCommitContentListener false
|
||||
}
|
||||
}
|
||||
callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes)
|
||||
ic = InputConnectionCompat.createWrapper(this, ic, editorInfo)
|
||||
|
||||
val onReceiveContentListener = OnReceiveContentListener { _, payload ->
|
||||
val split = payload.partition { item -> item.uri != null }
|
||||
val uriContent = split.first
|
||||
val remaining = split.second
|
||||
|
||||
if (uriContent != null) {
|
||||
val clip: ClipData = uriContent.clip
|
||||
for (i in 0 until clip.itemCount) {
|
||||
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 {
|
||||
|
|
|
@ -482,7 +482,7 @@ class RoomListFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAcceptRoomInvitation(room: RoomSummary) {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||
roomListViewModel.handle(RoomListAction.AcceptInvitation(room))
|
||||
}
|
||||
|
||||
|
@ -495,7 +495,7 @@ class RoomListFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onRejectRoomInvitation(room: RoomSummary) {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||
roomListViewModel.handle(RoomListAction.RejectInvitation(room))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
*/
|
||||
package im.vector.app.features.notifications
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.takeAs
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
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.EventType
|
||||
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.room.model.Membership
|
||||
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.timeline.TimelineEvent
|
||||
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.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import timber.log.Timber
|
||||
|
@ -49,11 +54,12 @@ import javax.inject.Inject
|
|||
class NotifiableEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val noticeEventFormatter: NoticeEventFormatter,
|
||||
private val displayableEventFormatter: DisplayableEventFormatter) {
|
||||
private val displayableEventFormatter: DisplayableEventFormatter
|
||||
) {
|
||||
|
||||
// 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 eventId = event.eventId ?: return null
|
||||
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
|
||||
|
||||
// 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...)
|
||||
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
||||
|
||||
|
@ -140,6 +146,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
senderName = senderDisplayName,
|
||||
senderId = event.root.senderId,
|
||||
body = body.toString(),
|
||||
imageUri = event.fetchImageIfPresent(session),
|
||||
roomId = event.root.roomId!!,
|
||||
roomName = roomName,
|
||||
matrixID = session.myUserId
|
||||
|
@ -173,6 +180,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
senderName = senderDisplayName,
|
||||
senderId = event.root.senderId,
|
||||
body = body,
|
||||
imageUri = event.fetchImageIfPresent(session),
|
||||
roomId = event.root.roomId!!,
|
||||
roomName = roomName,
|
||||
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? {
|
||||
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
||||
val roomId = event.roomId ?: return null
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package im.vector.app.features.notifications
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
|
||||
data class NotifiableMessageEvent(
|
||||
|
@ -26,6 +27,7 @@ data class NotifiableMessageEvent(
|
|||
val senderName: String?,
|
||||
val senderId: String?,
|
||||
val body: String?,
|
||||
val imageUri: Uri?,
|
||||
val roomId: String,
|
||||
val roomName: String?,
|
||||
val roomIsDirect: Boolean = false,
|
||||
|
|
|
@ -49,26 +49,26 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
NotificationUtils.SMART_REPLY_ACTION ->
|
||||
handleSmartReply(intent, context)
|
||||
NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) }
|
||||
}
|
||||
NotificationUtils.DISMISS_SUMMARY_ACTION ->
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
NotificationUtils.MARK_ROOM_READ_ACTION ->
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMessageEventOfRoom(it)
|
||||
handleMarkAsRead(it)
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) }
|
||||
handleMarkAsRead(roomId)
|
||||
}
|
||||
NotificationUtils.JOIN_ACTION -> {
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(it)
|
||||
handleJoinRoom(it)
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) }
|
||||
handleJoinRoom(roomId)
|
||||
}
|
||||
}
|
||||
NotificationUtils.REJECT_ACTION -> {
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(it)
|
||||
handleRejectRoom(it)
|
||||
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
|
||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) }
|
||||
handleRejectRoom(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +138,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
?: context?.getString(R.string.notification_sender_me),
|
||||
senderId = session.myUserId,
|
||||
body = message,
|
||||
imageUri = null,
|
||||
roomId = room.roomId,
|
||||
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
||||
roomIsDirect = room.roomSummary()?.isDirect == true,
|
||||
|
@ -145,8 +146,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
canBeReplaced = false
|
||||
)
|
||||
|
||||
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) }
|
||||
|
||||
/*
|
||||
// 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.
|
||||
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()) {
|
||||
Timber.i("Notification are disabled for this device")
|
||||
return
|
||||
|
@ -105,87 +105,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
} else {
|
||||
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) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
add(notifiableEvent, seenEventIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all known events and refresh the notification drawer
|
||||
*/
|
||||
fun clearAllEvents() {
|
||||
synchronized(queuedEvents) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
updateEvents { it.clear() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*/
|
||||
fun setCurrentRoom(roomId: String?) {
|
||||
var hasChanged: Boolean
|
||||
synchronized(queuedEvents) {
|
||||
hasChanged = roomId != currentRoomId
|
||||
updateEvents {
|
||||
val hasChanged = roomId != currentRoomId
|
||||
currentRoomId = roomId
|
||||
}
|
||||
if (hasChanged) {
|
||||
clearMessageEventOfRoom(roomId)
|
||||
if (hasChanged && roomId != null) {
|
||||
it.clearMessagesForRoom(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMemberShipNotificationForRoom(roomId: String) {
|
||||
val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId }
|
||||
if (shouldUpdate) {
|
||||
refreshNotificationDrawerBg()
|
||||
fun notificationStyleChanged() {
|
||||
updateEvents {
|
||||
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
||||
if (newSettings != useCompleteNotificationFormat) {
|
||||
// Settings has changed, remove all current notifications
|
||||
notificationDisplayer.cancelAllNotifications()
|
||||
useCompleteNotificationFormat = newSettings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean {
|
||||
return synchronized(queuedEvents) {
|
||||
queuedEvents.removeAll(predicate)
|
||||
fun updateEvents(action: NotificationDrawerManager.(NotificationEventQueue) -> Unit) {
|
||||
synchronized(queuedEvents) {
|
||||
action(this, queuedEvents)
|
||||
}
|
||||
refreshNotificationDrawer()
|
||||
}
|
||||
|
||||
private var firstThrottler = FirstThrottler(200)
|
||||
|
||||
fun refreshNotificationDrawer() {
|
||||
private fun refreshNotificationDrawer() {
|
||||
// Implement last throttler
|
||||
val canHandle = firstThrottler.canHandle()
|
||||
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
|
||||
|
@ -239,18 +171,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
@WorkerThread
|
||||
private fun 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) {
|
||||
notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also {
|
||||
queuedEvents.clear()
|
||||
queuedEvents.addAll(it.onlyKeptEvents())
|
||||
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, renderedEvents).also {
|
||||
queuedEvents.clearAndAdd(it.onlyKeptEvents())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (!file.exists()) file.createNewFile()
|
||||
FileOutputStream(file).use {
|
||||
currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it)
|
||||
currentSession?.securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
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 {
|
||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (file.exists()) {
|
||||
file.inputStream().use {
|
||||
val events: ArrayList<NotifiableEvent>? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
||||
if (events != null) {
|
||||
return events.toMutableList()
|
||||
return NotificationEventQueue(events.toMutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Failed to load cached notification info")
|
||||
}
|
||||
return ArrayList()
|
||||
return NotificationEventQueue()
|
||||
}
|
||||
|
||||
private fun deleteCachedRoomNotifications() {
|
||||
|
@ -330,11 +253,3 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
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
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||
|
@ -104,7 +103,7 @@ class NotificationFactory @Inject constructor(
|
|||
|
||||
sealed interface 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(
|
||||
val summaryLine: CharSequence,
|
||||
val messageCount: Int,
|
||||
|
|
|
@ -17,7 +17,6 @@ package im.vector.app.features.notifications
|
|||
|
||||
import android.content.Context
|
||||
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_INVITATION_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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment
|
|||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
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.services.CallService
|
||||
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.
|
||||
val i = HomeActivity.newIntent(context)
|
||||
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)
|
||||
|
||||
|
@ -320,16 +321,23 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
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)
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.addNextIntent(VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
call = call,
|
||||
mode = VectorCallActivity.INCOMING_ACCEPT)
|
||||
.addNextIntent(
|
||||
VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
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)
|
||||
|
||||
|
@ -338,7 +346,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||
getActionText(R.string.call_notification_reject, R.attr.colorError),
|
||||
rejectCallPendingIntent)
|
||||
rejectCallPendingIntent
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
|
@ -390,7 +404,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
||||
rejectCallPendingIntent)
|
||||
rejectCallPendingIntent
|
||||
)
|
||||
)
|
||||
builder.setContentIntent(contentPendingIntent)
|
||||
|
||||
|
@ -431,13 +446,14 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
IconCompat.createWithResource(context, R.drawable.ic_call_hangup)
|
||||
.setTint(ThemeUtils.getColor(context, R.attr.colorError)),
|
||||
getActionText(R.string.call_notification_hangup, R.attr.colorError),
|
||||
rejectCallPendingIntent)
|
||||
rejectCallPendingIntent
|
||||
)
|
||||
)
|
||||
|
||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.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)
|
||||
|
||||
|
@ -453,7 +469,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
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)
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.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)
|
||||
return builder.build()
|
||||
|
@ -517,7 +533,10 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
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 {
|
||||
setContentIntent(it)
|
||||
}
|
||||
|
@ -587,8 +606,12 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
markRoomReadIntent.action = MARK_ROOM_READ_ACTION
|
||||
markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId)
|
||||
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
||||
val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val markRoomReadPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
markRoomReadIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
NotificationCompat.Action.Builder(R.drawable.ic_material_done_all_white,
|
||||
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)
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
|
||||
intent.action = DISMISS_ROOM_NOTIF_ACTION
|
||||
val pendingIntent = PendingIntent.getBroadcast(context.applicationContext,
|
||||
System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
System.currentTimeMillis().toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
setDeleteIntent(pendingIntent)
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
|
@ -655,31 +682,41 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
rejectIntent.action = REJECT_ACTION
|
||||
rejectIntent.data = createIgnoredUri("$roomId&$matrixId")
|
||||
rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||
val rejectIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), rejectIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val rejectIntentPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
rejectIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
addAction(
|
||||
R.drawable.vector_notification_reject_invitation,
|
||||
stringProvider.getString(R.string.reject),
|
||||
rejectIntentPendingIntent)
|
||||
rejectIntentPendingIntent
|
||||
)
|
||||
|
||||
// offer to type a quick accept button
|
||||
val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||
joinIntent.action = JOIN_ACTION
|
||||
joinIntent.data = createIgnoredUri("$roomId&$matrixId")
|
||||
joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||
val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val joinIntentPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
joinIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
addAction(
|
||||
R.drawable.vector_notification_accept_invitation,
|
||||
stringProvider.getString(R.string.join),
|
||||
joinIntentPendingIntent)
|
||||
joinIntentPendingIntent
|
||||
)
|
||||
|
||||
val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId)
|
||||
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
|
||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||
|
||||
if (inviteNotifiableEvent.noisy) {
|
||||
// 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
|
||||
// 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)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
|
||||
|
||||
if (simpleNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
|
@ -745,14 +782,22 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.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 {
|
||||
val intent = HomeActivity.newIntent(context, clearNotification = true)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
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.data = createIgnoredUri(roomId)
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
|
||||
return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
intent,
|
||||
// PendingIntents attached to actions with remote inputs must be mutable
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
|
||||
)
|
||||
} else {
|
||||
/*
|
||||
TODO
|
||||
|
@ -783,7 +833,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
// the action must be unique else the parameters are ignored
|
||||
quickReplyIntent.action = QUICK_LAUNCH_ACTION
|
||||
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)
|
||||
intent.action = DISMISS_SUMMARY_ACTION
|
||||
intent.data = createIgnoredUri("deleteSummary")
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
|
||||
|
@ -875,7 +929,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
context,
|
||||
0,
|
||||
testActionIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
notificationManager.notify(
|
||||
|
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
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.getActions
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -31,45 +36,36 @@ class PushRuleTriggerListener @Inject constructor(
|
|||
) : PushRuleService.PushRuleListener {
|
||||
|
||||
private var session: Session? = null
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
|
||||
override fun onMatchRule(event: Event, actions: List<Action>) {
|
||||
Timber.v("Push rule match for event ${event.eventId}")
|
||||
val safeSession = session ?: return Unit.also {
|
||||
Timber.e("Called without active session")
|
||||
override fun onEvents(pushEvents: PushEvents) {
|
||||
scope.launch {
|
||||
session?.let { 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()
|
||||
if (notificationAction.shouldNotify) {
|
||||
val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank())
|
||||
if (notifiableEvent == null) {
|
||||
Timber.v("## Failed to resolve event")
|
||||
// TODO
|
||||
private suspend fun createNotifiableEvents(pushEvents: PushEvents, session: Session): List<NotifiableEvent> {
|
||||
return pushEvents.matchedEvents.mapNotNull { (event, pushRule) ->
|
||||
Timber.v("Push rule match for event ${event.eventId}")
|
||||
val action = pushRule.getActions().toNotificationAction()
|
||||
if (action.shouldNotify) {
|
||||
resolver.resolveEvent(event, session, isNoisy = !action.soundName.isNullOrBlank())
|
||||
} else {
|
||||
Timber.v("New event to notify")
|
||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
Timber.v("Matched push rule is set to not notify")
|
||||
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) {
|
||||
if (this.session != null) {
|
||||
stop()
|
||||
|
@ -79,6 +75,7 @@ class PushRuleTriggerListener @Inject constructor(
|
|||
}
|
||||
|
||||
fun stop() {
|
||||
scope.coroutineContext.cancelChildren(CancellationException("PushRuleTriggerListener stopping"))
|
||||
session?.removePushRuleListener(this)
|
||||
session = null
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
|
|
|
@ -18,14 +18,10 @@ package im.vector.app.features.notifications
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
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.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 timber.log.Timber
|
||||
|
@ -61,17 +57,6 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
|
||||
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 smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||
|
@ -96,7 +81,6 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
userDisplayName,
|
||||
tickerText
|
||||
),
|
||||
shortcutInfo,
|
||||
meta
|
||||
)
|
||||
}
|
||||
|
@ -114,7 +98,14 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
when {
|
||||
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
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -41,7 +40,7 @@ private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L
|
|||
class PinLocker @Inject constructor(
|
||||
private val pinCodeStore: PinCodeStore,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : LifecycleObserver {
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
enum class State {
|
||||
// App is locked, can be unlock
|
||||
|
@ -87,16 +86,14 @@ class PinLocker @Inject constructor(
|
|||
computeState()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
|
||||
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod()
|
||||
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked")
|
||||
computeState()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Timber.v("App enters background")
|
||||
entersBackgroundTs = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
|
|
@ -61,8 +61,8 @@ class CreatePollFragment @Inject constructor(
|
|||
viewModel.handle(CreatePollAction.OnCreatePoll)
|
||||
}
|
||||
|
||||
viewModel.subscribe(this) {
|
||||
views.createPollButton.isEnabled = it.canCreatePoll
|
||||
viewModel.onEach(CreatePollViewState::canCreatePoll) { canCreatePoll ->
|
||||
views.createPollButton.isEnabled = canCreatePoll
|
||||
}
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
|
|
|
@ -46,7 +46,7 @@ class RageShake @Inject constructor(private val activity: FragmentActivity,
|
|||
|
||||
shakeDetector = ShakeDetector(this).apply {
|
||||
setSensitivity(vectorPreferences.getRageshakeSensitivity())
|
||||
start(sensorManager)
|
||||
start(sensorManager, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class VectorSettingsPinFragment @Inject constructor(
|
|||
|
||||
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
||||
// Refresh the drawer for an immediate effect of this change
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
notificationDrawerManager.notificationStyleChanged()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import java.io.File
|
|||
import java.io.FileOutputStream
|
||||
|
||||
abstract class AbstractVoiceRecorder(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
private val filenameExt: String
|
||||
) : VoiceRecorder {
|
||||
private val outputDirectory: File by lazy {
|
||||
|
@ -39,7 +39,7 @@ abstract class AbstractVoiceRecorder(
|
|||
abstract fun convertFile(recordedFile: File?): File?
|
||||
|
||||
private fun init() {
|
||||
MediaRecorder().let {
|
||||
createMediaRecorder().let {
|
||||
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
|
||||
setOutputFormat(it)
|
||||
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() {
|
||||
init()
|
||||
outputFile = File(outputDirectory, "Voice message.$filenameExt")
|
||||
|
|
|
@ -69,11 +69,11 @@
|
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toTopOf="@id/createPollButton"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
||||
tools:listitem="@layout/item_profile_action" />
|
||||
tools:listitem="@layout/item_form_text_input_with_delete" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/createPollButton"
|
||||
style="@style/Widget.Vector.Button.CreatePoll"
|
||||
style="@style/Widget.Vector.Button.CallToAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_margin="16dp"
|
||||
|
@ -92,7 +92,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/formTextInputTextInputLayout"
|
||||
style="@style/Widget.Vector.EditText.Form"
|
||||
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/formTextInputTextInputLayout"
|
||||
style="@style/Widget.Vector.EditText.Form"
|
||||
style="@style/Widget.Vector.TextInputLayout.Form"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
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_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 -->
|
||||
<string name="room_sliding_menu_messages">Messages</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.test.fakes.FakeAutoAcceptInvites
|
||||
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.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
|
@ -145,48 +148,3 @@ class NotifiableEventProcessorTest {
|
|||
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.FakeRoomGroupMessageCreator
|
||||
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.junit.Test
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue