mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 02:45:53 +03:00
Merge branch 'release/1.0.6'
This commit is contained in:
commit
0da106ba36
337 changed files with 12078 additions and 4166 deletions
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,6 +1,6 @@
|
|||
### Pull Request Checklist
|
||||
|
||||
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||
|
||||
- [ ] Changes has been tested on an Android device or Android emulator with API 21
|
||||
- [ ] UI change has been tested on both light and dark themes
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
A full developer contributors list can be found [here](https://github.com/vector-im/riotX-android/graphs/contributors).
|
||||
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
|
||||
|
||||
# Core team:
|
||||
|
||||
|
|
49
CHANGES.md
49
CHANGES.md
|
@ -1,3 +1,52 @@
|
|||
Changes in Element 1.0.6 (2020-09-08)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- List phone numbers and emails added to the Matrix account, and add emails and phone numbers to account (#44, #45)
|
||||
|
||||
Improvements 🙌:
|
||||
- You can now join room through permalink and within room directory search
|
||||
- Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774)
|
||||
- Fix several issues when uploading big files (#1889)
|
||||
- Do not propose to verify session if there is only one session and 4S is not configured (#1901)
|
||||
- Call screen does not use proximity sensor (#1735)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Display name not shown under Settings/General (#1926)
|
||||
- Editing message forgets line breaks and markdown (#1939)
|
||||
- Words containing my name should not trigger notifications (#1781)
|
||||
- Fix changing language issue
|
||||
- Fix FontSize issue (#1483, #1787)
|
||||
- Fix bad color for settings icon on Android < 24 (#1786)
|
||||
- Change user or room avatar: when selecting Gallery, I'm not proposed to crop the selected image (#1590)
|
||||
- Loudspeaker is always used (#1685)
|
||||
- Fix uploads still don't work with room v6 (#1879)
|
||||
- Can't handle ongoing call events in background (#1992)
|
||||
- Handle room, user and group links by the Element app (#1795)
|
||||
- Update associated site domain (#1833)
|
||||
- Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034
|
||||
- Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027)
|
||||
- Support for image compression on Android 10
|
||||
- Verification popup won't show
|
||||
- Android 6: App crash when read Contact permission is granted (#2064)
|
||||
- JSON for verification events leaks in to the room list (#1246)
|
||||
- Replies to poll appears in timeline as unsupported events during sending (#1004)
|
||||
|
||||
Translations 🗣:
|
||||
- The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909)
|
||||
- New translation to kabyle
|
||||
|
||||
Build 🧱:
|
||||
- Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging)
|
||||
- Buildkite:
|
||||
New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml
|
||||
New build location: https://buildkite.com/matrix-dot-org/element-android
|
||||
|
||||
Other changes:
|
||||
- Use File extension functions to make code more concise (#1996)
|
||||
- Create a script to import SAS strings (#1909)
|
||||
- Support `data-mx-[bg-]color` attributes on `<font>` tags.
|
||||
|
||||
Changes in Element 1.0.5 (2020-08-21)
|
||||
===================================================
|
||||
|
||||
|
|
16
README.md
16
README.md
|
@ -1,9 +1,9 @@
|
|||
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
|
||||
[![Weblate](https://translate.riot.im/widgets/element-android/-/svg-badge.svg)](https://translate.riot.im/engage/element-android/?utm_source=widget)
|
||||
[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
|
||||
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
|
||||
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
|
||||
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
|
||||
|
||||
# Element Android
|
||||
|
||||
|
@ -14,11 +14,13 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
|
|||
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
||||
|
||||
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
|
||||
|
||||
# New Android SDK
|
||||
|
||||
Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
|
||||
Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository.
|
||||
|
||||
At each Element release, the SDK module is copied to a dedicated repository: https://github.com/matrix-org/matrix-android-sdk2. That way, third party apps can add a regular gradle dependency to use it. So more details on how to do that here: https://github.com/matrix-org/matrix-android-sdk2.
|
||||
|
||||
# Roadmap
|
||||
|
||||
|
@ -27,6 +29,6 @@ The team will work to add them on a regular basis.
|
|||
|
||||
## Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
||||
|
|
|
@ -27,4 +27,9 @@ class AnimatedImageViewHolder constructor(itemView: View) :
|
|||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
touchImageView.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
|
|||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
holder.touchImageView.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
|
@ -77,11 +78,13 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
|
|||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
holder.touchImageView.setImageDrawable(errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
|
|
|
@ -35,6 +35,7 @@ interface VideoLoaderTarget {
|
|||
fun onVideoFileLoading(uid: String)
|
||||
fun onVideoFileLoadFailed(uid: String)
|
||||
fun onVideoFileReady(uid: String, file: File)
|
||||
fun onVideoURLReady(uid: String, path: String)
|
||||
}
|
||||
|
||||
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
|
||||
|
@ -47,6 +48,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
|
|||
}
|
||||
|
||||
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
|
||||
|
@ -68,9 +71,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
|
|||
|
||||
override fun onVideoFileReady(uid: String, file: File) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
arrangeForVideoReady()
|
||||
holder.videoReady(file)
|
||||
}
|
||||
|
||||
override fun onVideoURLReady(uid: String, path: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
arrangeForVideoReady()
|
||||
holder.videoReady(path)
|
||||
}
|
||||
|
||||
private fun arrangeForVideoReady() {
|
||||
holder.thumbnailImage.isVisible = false
|
||||
holder.loaderProgressBar.isVisible = false
|
||||
holder.videoView.isVisible = true
|
||||
holder.videoReady(file)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.lib.attachmentviewer
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
@ -65,6 +66,13 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
}
|
||||
}
|
||||
|
||||
fun videoReady(path: String) {
|
||||
mVideoPath = path
|
||||
if (isSelected) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
fun videoFileLoadError() {
|
||||
}
|
||||
|
||||
|
@ -118,8 +126,13 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
}
|
||||
try {
|
||||
videoView.setVideoPath(mVideoPath)
|
||||
} catch (failure: Throwable) {
|
||||
// Couldn't open
|
||||
Log.v(VideoViewHolder::class.java.name, "Failed to start video")
|
||||
}
|
||||
|
||||
videoView.setVideoPath(mVideoPath)
|
||||
if (!wasPaused) {
|
||||
videoView.start()
|
||||
if (progress > 0) {
|
||||
|
|
|
@ -39,4 +39,9 @@ class ZoomableImageViewHolder constructor(itemView: View) :
|
|||
}
|
||||
|
||||
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
touchImageView.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
|
21
build.gradle
21
build.gradle
|
@ -81,15 +81,15 @@ apply plugin: 'org.sonarqube'
|
|||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectName", "RiotX-Android"
|
||||
property "sonar.projectKey", "vector.android.riotx"
|
||||
property "sonar.projectName", "Element-Android"
|
||||
property "sonar.projectKey", "im.vector.app.android"
|
||||
property "sonar.host.url", "https://sonarcloud.io"
|
||||
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
|
||||
property "sonar.sourceEncoding", "UTF-8"
|
||||
property "sonar.links.homepage", "https://github.com/vector-im/riotX-android/"
|
||||
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android"
|
||||
property "sonar.links.scm", "https://github.com/vector-im/riotX-android/"
|
||||
property "sonar.links.issue", "https://github.com/vector-im/riotX-android/issues"
|
||||
property "sonar.links.homepage", "https://github.com/vector-im/element-android/"
|
||||
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android"
|
||||
property "sonar.links.scm", "https://github.com/vector-im/element-android/"
|
||||
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
|
||||
property "sonar.organization", "new_vector_ltd_organization"
|
||||
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
|
||||
}
|
||||
|
@ -100,11 +100,18 @@ project(":vector") {
|
|||
properties {
|
||||
property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs
|
||||
// exclude source code from analyses separated by a colon (:)
|
||||
// property "sonar.exclusions", "**/*.*"
|
||||
// Exclude Java source
|
||||
property "sonar.exclusions", "**/BugReporterMultipartBody.java"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project(":diff-match-patch") {
|
||||
sonarqube {
|
||||
skipProject = true
|
||||
}
|
||||
}
|
||||
|
||||
//project(":matrix-sdk-android") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
|
|
285
docs/add_threePids.md
Normal file
285
docs/add_threePids.md
Normal file
|
@ -0,0 +1,285 @@
|
|||
# Adding and removing ThreePids to an account
|
||||
|
||||
## Add email
|
||||
|
||||
### User enter the email
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/email/requestToken
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "alice@email-provider.org",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
|
||||
"send_attempt": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### The email is already added to an account
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_IN_USE",
|
||||
"error": "Email is already in use"
|
||||
}
|
||||
```
|
||||
|
||||
#### The email is free
|
||||
|
||||
Wording: "We've sent you an email to verify your address. Please follow the instructions there and then click the button below."
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ"
|
||||
}
|
||||
```
|
||||
|
||||
## User receive an e-mail
|
||||
|
||||
> [homeserver.org] Validate your email
|
||||
>
|
||||
> A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email:
|
||||
https://homeserver.org/_matrix/client/unstable/add_threepid/email/submit_token?token=WUnEhQAmJrXupdEbXgdWvnVIKaGYZFsU&client_secret=TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh&sid=bxyDHuJKsdkjMlTJ
|
||||
>
|
||||
> If this was not you, you can safely ignore this email. Thank you.
|
||||
|
||||
### User clicks on the link
|
||||
|
||||
The browser displays the following message:
|
||||
|
||||
> Your email has now been validated, please return to your client. You may now close this window.
|
||||
|
||||
### User returns on Element
|
||||
|
||||
User clicks on CONTINUE
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/add
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh"
|
||||
}
|
||||
```
|
||||
|
||||
401 User Interactive Authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"session": "ppvvnozXCQZFaggUBlHJYPjA",
|
||||
"flows": [
|
||||
{
|
||||
"stages": [
|
||||
"m.login.password"
|
||||
]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User enters his password
|
||||
|
||||
POST https://homeserver.org/_matrix/client/r0/account/3pid/add
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "bxyDHuJKsdkjMlTJ",
|
||||
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
|
||||
"auth": {
|
||||
"session": "ppvvnozXCQZFaggUBlHJYPjA",
|
||||
"type": "m.login.password",
|
||||
"user": "@benoitx:matrix.org",
|
||||
"password": "weak_password"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### The link has not been clicked
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_AUTH_FAILED",
|
||||
"error": "No validated 3pid session found"
|
||||
}
|
||||
```
|
||||
|
||||
#### Wrong password
|
||||
|
||||
401
|
||||
|
||||
```json
|
||||
{
|
||||
"session": "fXHOvoQsPMhEebVqTnIrzZJN",
|
||||
"flows": [
|
||||
{
|
||||
"stages": [
|
||||
"m.login.password"
|
||||
]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
},
|
||||
"completed":[
|
||||
],
|
||||
"error": "Invalid password",
|
||||
"errcode": "M_FORBIDDEN"
|
||||
}
|
||||
```
|
||||
|
||||
#### The link has been clicked and the account password is correct
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
## Remove email
|
||||
|
||||
### User want to remove an email from his account
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/delete
|
||||
|
||||
```json
|
||||
{
|
||||
"medium": "email",
|
||||
"address": "alice@email-provider.org"
|
||||
}
|
||||
```
|
||||
|
||||
#### Email was not bound to an identity server
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"id_server_unbind_result": "no-support"
|
||||
}
|
||||
```
|
||||
|
||||
#### Email was bound to an identity server
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"id_server_unbind_result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## Add phone number
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/r0/account/3pid/msisdn/requestToken
|
||||
|
||||
```json
|
||||
{
|
||||
"country": "FR",
|
||||
"phone_number": "611223344",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"send_attempt": 1
|
||||
}
|
||||
```
|
||||
|
||||
Note that the phone number is sent without `+` and without the country code
|
||||
|
||||
#### The phone number is already added to an account
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_THREEPID_IN_USE",
|
||||
"error": "MSISDN is already in use"
|
||||
}
|
||||
```
|
||||
|
||||
#### The phone number is free
|
||||
|
||||
Wording: "A text message has been sent to +33611223344. Please enter the verification code it contains."
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"msisdn": "33651547677",
|
||||
"intl_fmt": "+33 6 51 54 76 77",
|
||||
"success": true,
|
||||
"sid": "253299954",
|
||||
"submit_url": "https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token"
|
||||
}
|
||||
```
|
||||
|
||||
## User receive a text message
|
||||
|
||||
> Riot
|
||||
|
||||
> Your Riot validation code is 892541, please enter this into the app
|
||||
|
||||
### User enter the code to the app
|
||||
|
||||
#### Wrong code
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "253299954",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"token": "111111"
|
||||
}
|
||||
```
|
||||
|
||||
400
|
||||
|
||||
```json
|
||||
{
|
||||
"errcode": "M_UNKNOWN",
|
||||
"error": "Error contacting the identity server"
|
||||
}
|
||||
```
|
||||
|
||||
This is not an ideal, but the client will display a hint to check the entered code to the user.
|
||||
|
||||
#### Correct code
|
||||
|
||||
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
|
||||
|
||||
```json
|
||||
{
|
||||
"sid": "253299954",
|
||||
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
|
||||
"token": "892541"
|
||||
}
|
||||
```
|
||||
|
||||
200
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow
|
||||
|
||||
## Remove phone number
|
||||
|
||||
### User wants to remove a phone number from his account
|
||||
|
||||
This is the same request and response than to remove email, but with this body:
|
||||
|
||||
```json
|
||||
{
|
||||
"medium": "msisdn",
|
||||
"address": "33611223344"
|
||||
}
|
||||
```
|
||||
|
||||
Note that the phone number is provided without `+`, but with the country code.
|
|
@ -8,7 +8,9 @@ This document describes the flow of signin to a homeserver, and also the flow wh
|
|||
|
||||
Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
|
||||
|
||||
> curl -X GET 'https://matrix.org/_matrix/client/r0/login'
|
||||
```shell script
|
||||
curl -X GET 'https://matrix.org/_matrix/client/r0/login'
|
||||
```
|
||||
|
||||
200
|
||||
|
||||
|
@ -26,7 +28,9 @@ Client request the sign-in flows, once the homeserver is chosen by the user and
|
|||
|
||||
The user is able to connect using `m.login.password`
|
||||
|
||||
> curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
|
||||
```shell script
|
||||
curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -73,14 +77,16 @@ We get credential (200)
|
|||
|
||||
If the user has associated an email with its account, he can signin using the email.
|
||||
|
||||
> curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
|
||||
```shell script
|
||||
curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@email-provider.org"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"identifier": {
|
||||
"type": "m.id.thirdparty",
|
||||
"medium": "email",
|
||||
"address": "alice@yopmail.com"
|
||||
"address": "alice@email-provider.org"
|
||||
},
|
||||
"password": "weak_password",
|
||||
"type": "m.login.password",
|
||||
|
@ -136,7 +142,9 @@ Not supported yet in Element
|
|||
|
||||
### Login with SSO
|
||||
|
||||
> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
```shell script
|
||||
curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
```
|
||||
|
||||
200
|
||||
|
||||
|
@ -171,7 +179,9 @@ Once the process is finished, the web page will call the `redirectUrl` with an e
|
|||
|
||||
This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
|
||||
|
||||
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
```shell script
|
||||
curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -214,7 +224,9 @@ We display a warning regarding e2e.
|
|||
|
||||
At the first step, we do not send the password, only the email and a client secret, generated by the application
|
||||
|
||||
> curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
|
||||
```shell script
|
||||
curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -251,7 +263,9 @@ During this step, the new password is sent to the homeserver.
|
|||
|
||||
If the user confirms before the link is clicked, we get an error:
|
||||
|
||||
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -285,7 +299,9 @@ It contains the client secret, a token and the sid
|
|||
|
||||
When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand):
|
||||
|
||||
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
@ -10,7 +10,9 @@ This document describes the flow of registration to a homeserver. Examples come
|
|||
|
||||
Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
|
||||
|
||||
> curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -70,7 +72,9 @@ If the registration is not possible, we get a 403
|
|||
|
||||
The app is displaying a form to enter username and password.
|
||||
|
||||
> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -133,9 +137,11 @@ We get a 400:
|
|||
|
||||
### Step 2: entering email
|
||||
|
||||
User is proposed to enter an email. We skip this step.
|
||||
User is proposed to enter an email. User skips this step.
|
||||
|
||||
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -189,16 +195,18 @@ User is proposed to enter an email. We skip this step.
|
|||
}
|
||||
```
|
||||
|
||||
### Step 2 bis: we enter an email
|
||||
### Step 2 bis: user enters an email
|
||||
|
||||
We request a token to the homeserver. The `client_secret` is generated by the application
|
||||
|
||||
> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken'
|
||||
```shell script
|
||||
curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@email-provider.org","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
|
||||
"email": "alice@yopmail.com",
|
||||
"email": "alice@email-provider.org",
|
||||
"send_attempt": 0
|
||||
}
|
||||
```
|
||||
|
@ -213,7 +221,9 @@ We request a token to the homeserver. The `client_secret` is generated by the ap
|
|||
|
||||
And
|
||||
|
||||
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -239,7 +249,9 @@ We get 401 since the email is not validated yet:
|
|||
|
||||
The app is now polling on
|
||||
|
||||
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -254,7 +266,7 @@ The app is now polling on
|
|||
}
|
||||
```
|
||||
|
||||
We click on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains:
|
||||
User clicks on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains:
|
||||
- A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ
|
||||
- The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa
|
||||
- A `sid`: qlBCREDACTEDEtgxD
|
||||
|
@ -306,7 +318,9 @@ Once the link is clicked, the registration request (polling) returns a 401 with
|
|||
|
||||
User is proposed to accept T&C and he accepts them
|
||||
|
||||
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -365,7 +379,9 @@ User is proposed to accept T&C and he accepts them
|
|||
|
||||
User is proposed to prove he is not a robot and he does it:
|
||||
|
||||
> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -396,9 +412,11 @@ Some homeservers may require the user to enter MSISDN.
|
|||
|
||||
On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration.
|
||||
|
||||
The user enter a phone number and select a country, the `client_secret` is generated by the application
|
||||
The user enters a phone number and selects a country, the `client_secret` is generated by the application
|
||||
|
||||
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
|
||||
```shell script
|
||||
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -430,10 +448,11 @@ If it is not the case, the homeserver send the SMS and returns some data, especi
|
|||
}
|
||||
```
|
||||
|
||||
When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet:
|
||||
|
||||
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
When we execute the register request, with the received `sid`, we get an error since the MSISDN is not validated yet:
|
||||
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
"auth": {
|
||||
|
@ -492,7 +511,9 @@ There is an issue on Synapse, which return a 401, it sends too much data along w
|
|||
|
||||
The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request:
|
||||
|
||||
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
|
||||
```shell script
|
||||
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -520,7 +541,9 @@ And if the code is correct we get a 200 with:
|
|||
|
||||
We can now execute the registration request, to the homeserver
|
||||
|
||||
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```shell script
|
||||
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -535,7 +558,7 @@ We can now execute the registration request, to the homeserver
|
|||
}
|
||||
```
|
||||
|
||||
Now the homeserver consider that the `m.login.msisdn` step is completed (401):
|
||||
Now the homeserver considers that the `m.login.msisdn` step is completed (401):
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
@ -18,9 +18,13 @@
|
|||
package org.matrix.android.sdk.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
|
@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional
|
|||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
|
@ -110,6 +110,11 @@ class RxSession(private val session: Session) {
|
|||
.startWithCallable { session.getThreePids() }
|
||||
}
|
||||
|
||||
fun livePendingThreePIds(): Observable<List<ThreePid>> {
|
||||
return session.getPendingThreePidsLive().asObservable()
|
||||
.startWithCallable { session.getPendingThreePids() }
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
|
||||
session.createRoom(roomParams, it)
|
||||
}
|
||||
|
|
|
@ -112,18 +112,18 @@ dependencies {
|
|||
def moshi_version = '1.8.0'
|
||||
def lifecycle_version = '2.2.0'
|
||||
def arch_version = '2.1.0'
|
||||
def coroutines_version = "1.3.2"
|
||||
def coroutines_version = "1.3.8"
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def work_version = '2.3.3'
|
||||
def work_version = '2.4.0'
|
||||
def retrofit_version = '2.6.2'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.core:core-ktx:1.3.0"
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.core:core-ktx:1.3.1"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
|
@ -131,8 +131,6 @@ dependencies {
|
|||
// Network
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
|
||||
|
||||
|
||||
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1"))
|
||||
implementation 'com.squareup.okhttp3:okhttp'
|
||||
|
@ -146,7 +144,6 @@ dependencies {
|
|||
|
||||
// Image
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||
implementation 'id.zelory:compressor:3.0.0'
|
||||
|
||||
// Database
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||
|
|
|
@ -23,15 +23,12 @@ import androidx.work.WorkManager
|
|||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
|
||||
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
||||
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.network.UserAgentHolder
|
||||
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
@ -96,9 +93,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
|||
fun getSdkVersion(): String {
|
||||
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
|
||||
}
|
||||
|
||||
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
|
||||
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,17 @@ package org.matrix.android.sdk.internal.crypto
|
|||
import android.os.MemoryFile
|
||||
import android.util.Base64
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
|
@ -41,29 +41,26 @@ import java.io.InputStream
|
|||
class AttachmentEncryptionTest {
|
||||
|
||||
private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String {
|
||||
val `in` = Base64.decode(input, Base64.DEFAULT)
|
||||
val inputAsByteArray = Base64.decode(input, Base64.DEFAULT)
|
||||
|
||||
val inputStream: InputStream
|
||||
|
||||
inputStream = if (`in`.isEmpty()) {
|
||||
ByteArrayInputStream(`in`)
|
||||
inputStream = if (inputAsByteArray.isEmpty()) {
|
||||
inputAsByteArray.inputStream()
|
||||
} else {
|
||||
val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size)
|
||||
memoryFile.outputStream.write(`in`)
|
||||
val memoryFile = MemoryFile("file" + System.currentTimeMillis(), inputAsByteArray.size)
|
||||
memoryFile.outputStream.write(inputAsByteArray)
|
||||
memoryFile.inputStream
|
||||
}
|
||||
|
||||
val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo)
|
||||
val decryptedStream = ByteArrayOutputStream()
|
||||
val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream)
|
||||
|
||||
assertNotNull(decryptedStream)
|
||||
assert(result)
|
||||
|
||||
val buffer = ByteArray(100)
|
||||
val toByteArray = decryptedStream.toByteArray()
|
||||
|
||||
val len = decryptedStream!!.read(buffer)
|
||||
|
||||
decryptedStream.close()
|
||||
|
||||
return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
|
||||
return Base64.encodeToString(toByteArray, 0, toByteArray.size, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -74,7 +71,7 @@ class AttachmentEncryptionTest {
|
|||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
key_ops = listOf("encrypt", "decrypt"),
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
ext = true
|
||||
),
|
||||
|
@ -93,7 +90,7 @@ class AttachmentEncryptionTest {
|
|||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
k = "__________________________________________8",
|
||||
key_ops = listOf("encrypt", "decrypt"),
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
ext = true
|
||||
),
|
||||
|
@ -112,7 +109,7 @@ class AttachmentEncryptionTest {
|
|||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
k = "__________________________________________8",
|
||||
key_ops = listOf("encrypt", "decrypt"),
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
ext = true
|
||||
),
|
||||
|
@ -133,7 +130,7 @@ class AttachmentEncryptionTest {
|
|||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
k = "__________________________________________8",
|
||||
key_ops = listOf("encrypt", "decrypt"),
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
ext = true
|
||||
),
|
||||
|
|
|
@ -17,23 +17,22 @@
|
|||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.commonmark.renderer.text.TextContentRenderer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
|
||||
/**
|
||||
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
|
||||
* we can add more tests to cover the edge cases.
|
||||
* Some tests are suffixed with `_not_passing`, maybe one day we will fix them...
|
||||
* Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the
|
||||
* formatted body, which is quite useless.
|
||||
* Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered.
|
||||
* Element Web should be used as a reference for expected results, but not always.
|
||||
* Also Element Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. We are doing
|
||||
* the same to be able to edit messages (See #1939)
|
||||
* See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
|
@ -46,8 +45,7 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
*/
|
||||
private val markdownParser = MarkdownParser(
|
||||
Parser.builder().build(),
|
||||
HtmlRenderer.builder().build(),
|
||||
TextContentRenderer.builder().build()
|
||||
HtmlRenderer.builder().build()
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -83,6 +81,15 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBoldNewLines() {
|
||||
testTypeNewLines(
|
||||
name = "bold",
|
||||
markdownPattern = "**",
|
||||
htmlExpectedTag = "strong"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseItalic() {
|
||||
testType(
|
||||
|
@ -92,14 +99,23 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseItalicNewLines() {
|
||||
testTypeNewLines(
|
||||
name = "italic",
|
||||
markdownPattern = "*",
|
||||
htmlExpectedTag = "em"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseItalic2() {
|
||||
// Riot-Web format
|
||||
"_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>")
|
||||
// Element Web format
|
||||
"_italic_".let { markdownParser.parse(it).expect(it, "<em>italic</em>") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: the test is not passing, it does not work on Riot-Web neither
|
||||
* Note: the test is not passing, it does not work on Element Web neither
|
||||
*/
|
||||
@Test
|
||||
fun parseStrike_not_passing() {
|
||||
|
@ -110,14 +126,30 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseStrikeNewLines() {
|
||||
testTypeNewLines(
|
||||
name = "strike",
|
||||
markdownPattern = "~~",
|
||||
htmlExpectedTag = "del"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode() {
|
||||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "`",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCodeNewLines() {
|
||||
testTypeNewLines(
|
||||
name = "code",
|
||||
markdownPattern = "`",
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -126,9 +158,16 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "``",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode2NewLines() {
|
||||
testTypeNewLines(
|
||||
name = "code",
|
||||
markdownPattern = "``",
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -137,78 +176,85 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "```",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode3NewLines() {
|
||||
testTypeNewLines(
|
||||
name = "code",
|
||||
markdownPattern = "```",
|
||||
htmlExpectedTag = "code"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseUnorderedList() {
|
||||
"- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") }
|
||||
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
|
||||
"- item1".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n</ul>") }
|
||||
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n<li>item2</li>\n</ul>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseOrderedList() {
|
||||
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
|
||||
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
|
||||
"1. item1".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n</ol>") }
|
||||
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n<li>item2</li>\n</ol>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHorizontalLine() {
|
||||
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
|
||||
"---".let { markdownParser.parse(it).expect(it, "<hr />") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseH2AndContent() {
|
||||
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
|
||||
"a\n---\nb".let { markdownParser.parse(it).expect(it, "<h2>a</h2>\n<p>b</p>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQuote() {
|
||||
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
|
||||
"> quoted".let { markdownParser.parse(it).expect(it, "<blockquote>\n<p>quoted</p>\n</blockquote>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQuote_not_passing() {
|
||||
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
|
||||
"> quoted\nline2".let { markdownParser.parse(it).expect(it, "<blockquote><p>quoted<br />line2</p></blockquote>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBoldItalic() {
|
||||
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
|
||||
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
|
||||
"*italic* **bold**".let { markdownParser.parse(it).expect(it, "<em>italic</em> <strong>bold</strong>") }
|
||||
"**bold** *italic*".let { markdownParser.parse(it).expect(it, "<strong>bold</strong> <em>italic</em>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHead() {
|
||||
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
|
||||
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
|
||||
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
|
||||
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
|
||||
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
|
||||
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
|
||||
"# head1".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>") }
|
||||
"## head2".let { markdownParser.parse(it).expect(it, "<h2>head2</h2>") }
|
||||
"### head3".let { markdownParser.parse(it).expect(it, "<h3>head3</h3>") }
|
||||
"#### head4".let { markdownParser.parse(it).expect(it, "<h4>head4</h4>") }
|
||||
"##### head5".let { markdownParser.parse(it).expect(it, "<h5>head5</h5>") }
|
||||
"###### head6".let { markdownParser.parse(it).expect(it, "<h6>head6</h6>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHeads() {
|
||||
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
|
||||
"# head1\n# head2".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>\n<h1>head2</h1>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBoldNewLines_not_passing() {
|
||||
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
|
||||
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseLinks() {
|
||||
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
|
||||
"[link](target)".let { markdownParser.parse(it).expect(it, """<a href="target">link</a>""") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseParagraph() {
|
||||
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
|
||||
"# head\ncontent".let { markdownParser.parse(it).expect(it, "<h1>head</h1>\n<p>content</p>") }
|
||||
}
|
||||
|
||||
private fun testIdentity(text: String) {
|
||||
|
@ -217,59 +263,93 @@ class MarkdownParserTest : InstrumentedTest {
|
|||
|
||||
private fun testType(name: String,
|
||||
markdownPattern: String,
|
||||
htmlExpectedTag: String,
|
||||
plainTextPrefix: String = "",
|
||||
plainTextSuffix: String = "") {
|
||||
htmlExpectedTag: String) {
|
||||
// Test simple case
|
||||
"$markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
}
|
||||
|
||||
// Test twice the same tag
|
||||
"$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
|
||||
}
|
||||
|
||||
val textBefore = "a"
|
||||
val textAfter = "b"
|
||||
|
||||
// With sticked text before
|
||||
"$textBefore$markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
}
|
||||
|
||||
// With text before and space
|
||||
"$textBefore $markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
}
|
||||
|
||||
// With sticked text after
|
||||
"$markdownPattern$name$markdownPattern$textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
}
|
||||
|
||||
// With space and text after
|
||||
"$markdownPattern$name$markdownPattern $textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
}
|
||||
|
||||
// With sticked text before and text after
|
||||
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
|
||||
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
}
|
||||
|
||||
// With text before and after, with spaces
|
||||
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
}
|
||||
}
|
||||
|
||||
private fun testTypeNewLines(name: String,
|
||||
markdownPattern: String,
|
||||
htmlExpectedTag: String) {
|
||||
// With new line inside the block
|
||||
"$markdownPattern$name\n$name$markdownPattern"
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name<br />$name</$htmlExpectedTag>")
|
||||
}
|
||||
|
||||
// With new line between two blocks
|
||||
"$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern"
|
||||
.let {
|
||||
markdownParser.parse(it)
|
||||
.expect(expectedText = it,
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {
|
||||
|
|
|
@ -26,13 +26,10 @@ import org.matrix.android.sdk.BuildConfig
|
|||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
|
||||
import org.matrix.android.sdk.internal.network.UserAgentHolder
|
||||
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
@ -97,9 +94,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
|||
fun getSdkVersion(): String {
|
||||
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
|
||||
}
|
||||
|
||||
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
|
||||
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ sealed class WellknownResult {
|
|||
/**
|
||||
* Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter.
|
||||
*/
|
||||
object FailPrompt : WellknownResult()
|
||||
data class FailPrompt(val homeServerUrl: String?, val wellKnown: WellKnown?) : WellknownResult()
|
||||
|
||||
/**
|
||||
* Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process.
|
||||
|
|
|
@ -24,21 +24,24 @@ sealed class Action {
|
|||
object DoNotNotify : Action()
|
||||
data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action()
|
||||
data class Highlight(val highlight: Boolean) : Action()
|
||||
|
||||
companion object {
|
||||
const val ACTION_NOTIFY = "notify"
|
||||
const val ACTION_DONT_NOTIFY = "dont_notify"
|
||||
const val ACTION_COALESCE = "coalesce"
|
||||
|
||||
// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks
|
||||
const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak"
|
||||
|
||||
const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound"
|
||||
const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight"
|
||||
|
||||
const val ACTION_OBJECT_VALUE_KEY = "value"
|
||||
const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
|
||||
const val ACTION_OBJECT_VALUE_VALUE_RING = "ring"
|
||||
}
|
||||
}
|
||||
|
||||
private const val ACTION_NOTIFY = "notify"
|
||||
private const val ACTION_DONT_NOTIFY = "dont_notify"
|
||||
private const val ACTION_COALESCE = "coalesce"
|
||||
|
||||
// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks
|
||||
private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak"
|
||||
|
||||
private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound"
|
||||
private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight"
|
||||
|
||||
private const val ACTION_OBJECT_VALUE_KEY = "value"
|
||||
private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#actions
|
||||
*
|
||||
|
@ -69,18 +72,18 @@ private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
|
|||
fun List<Action>.toJson(): List<Any> {
|
||||
return map { action ->
|
||||
when (action) {
|
||||
is Action.Notify -> ACTION_NOTIFY
|
||||
is Action.DoNotNotify -> ACTION_DONT_NOTIFY
|
||||
is Action.Sound -> {
|
||||
is Action.Notify -> Action.ACTION_NOTIFY
|
||||
is Action.DoNotNotify -> Action.ACTION_DONT_NOTIFY
|
||||
is Action.Sound -> {
|
||||
mapOf(
|
||||
ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND,
|
||||
ACTION_OBJECT_VALUE_KEY to action.sound
|
||||
Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND,
|
||||
Action.ACTION_OBJECT_VALUE_KEY to action.sound
|
||||
)
|
||||
}
|
||||
is Action.Highlight -> {
|
||||
is Action.Highlight -> {
|
||||
mapOf(
|
||||
ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT,
|
||||
ACTION_OBJECT_VALUE_KEY to action.highlight
|
||||
Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT,
|
||||
Action.ACTION_OBJECT_VALUE_KEY to action.highlight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -92,26 +95,26 @@ fun PushRule.getActions(): List<Action> {
|
|||
|
||||
actions.forEach { actionStrOrObj ->
|
||||
when (actionStrOrObj) {
|
||||
ACTION_NOTIFY -> Action.Notify
|
||||
ACTION_DONT_NOTIFY -> Action.DoNotNotify
|
||||
is Map<*, *> -> {
|
||||
when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) {
|
||||
ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> {
|
||||
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue ->
|
||||
Action.ACTION_NOTIFY -> Action.Notify
|
||||
Action.ACTION_DONT_NOTIFY -> Action.DoNotNotify
|
||||
is Map<*, *> -> {
|
||||
when (actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]) {
|
||||
Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> {
|
||||
(actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue ->
|
||||
Action.Sound(stringValue)
|
||||
}
|
||||
// When the value is not there, default sound (not specified by the spec)
|
||||
?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT)
|
||||
?: Action.Sound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT)
|
||||
}
|
||||
ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> {
|
||||
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue ->
|
||||
Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> {
|
||||
(actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue ->
|
||||
Action.Highlight(boolValue)
|
||||
}
|
||||
// When the value is not there, default is true, says the spec
|
||||
?: Action.Highlight(true)
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}")
|
||||
Timber.w("Unsupported set_tweak value ${actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,32 +18,8 @@ package org.matrix.android.sdk.api.pushrules
|
|||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
||||
abstract class Condition(val kind: Kind) {
|
||||
interface Condition {
|
||||
fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
|
||||
|
||||
enum class Kind(val value: String) {
|
||||
EventMatch("event_match"),
|
||||
ContainsDisplayName("contains_display_name"),
|
||||
RoomMemberCount("room_member_count"),
|
||||
SenderNotificationPermission("sender_notification_permission"),
|
||||
Unrecognised("");
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromString(value: String): Kind {
|
||||
return when (value) {
|
||||
"event_match" -> EventMatch
|
||||
"contains_display_name" -> ContainsDisplayName
|
||||
"room_member_count" -> RoomMemberCount
|
||||
"sender_notification_permission" -> SenderNotificationPermission
|
||||
else -> Unrecognised
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
|
||||
|
||||
open fun technicalDescription(): String {
|
||||
return "Kind: $kind"
|
||||
}
|
||||
fun technicalDescription(): String
|
||||
}
|
||||
|
|
|
@ -20,17 +20,15 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import timber.log.Timber
|
||||
import org.matrix.android.sdk.internal.util.caseInsensitiveFind
|
||||
|
||||
class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
|
||||
class ContainsDisplayNameCondition : Condition {
|
||||
|
||||
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveContainsDisplayNameCondition(event, this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "User is mentioned"
|
||||
}
|
||||
override fun technicalDescription() = "User is mentioned"
|
||||
|
||||
fun isSatisfied(event: Event, displayName: String): Boolean {
|
||||
val message = when (event.type) {
|
||||
|
@ -45,31 +43,6 @@ class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
|
|||
else -> null
|
||||
} ?: return false
|
||||
|
||||
return caseInsensitiveFind(displayName, message.body)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns whether a string contains an occurrence of another, as a standalone word, regardless of case.
|
||||
*
|
||||
* @param subString the string to search for
|
||||
* @param longString the string to search in
|
||||
* @return whether a match was found
|
||||
*/
|
||||
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
|
||||
// add sanity checks
|
||||
if (subString.isEmpty() || longString.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
|
||||
return regex.containsMatchIn(longString)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## caseInsensitiveFind() : failed")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return message.body.caseInsensitiveFind(displayName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ package org.matrix.android.sdk.api.pushrules
|
|||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.util.caseInsensitiveFind
|
||||
import org.matrix.android.sdk.internal.util.hasSpecialGlobChar
|
||||
import org.matrix.android.sdk.internal.util.simpleGlobToRegExp
|
||||
import timber.log.Timber
|
||||
|
||||
class EventMatchCondition(
|
||||
|
@ -29,16 +32,18 @@ class EventMatchCondition(
|
|||
* The glob-style pattern to match against. Patterns with no special glob characters should
|
||||
* be treated as having asterisks prepended and appended when testing the condition.
|
||||
*/
|
||||
val pattern: String
|
||||
) : Condition(Kind.EventMatch) {
|
||||
val pattern: String,
|
||||
/**
|
||||
* true to match only words. In this case pattern will not be considered as a glob
|
||||
*/
|
||||
val wordsOnly: Boolean
|
||||
) : Condition {
|
||||
|
||||
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveEventMatchCondition(event, this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "'$key' Matches '$pattern'"
|
||||
}
|
||||
override fun technicalDescription() = "'$key' matches '$pattern', words only '$wordsOnly'"
|
||||
|
||||
fun isSatisfied(event: Event): Boolean {
|
||||
// TODO encrypted events?
|
||||
|
@ -48,14 +53,18 @@ class EventMatchCondition(
|
|||
|
||||
// Patterns with no special glob characters should be treated as having asterisks prepended
|
||||
// and appended when testing the condition.
|
||||
try {
|
||||
val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*")
|
||||
val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL)
|
||||
return regex.containsMatchIn(value)
|
||||
return try {
|
||||
if (wordsOnly) {
|
||||
value.caseInsensitiveFind(pattern)
|
||||
} else {
|
||||
val modPattern = if (pattern.hasSpecialGlobChar()) pattern.simpleGlobToRegExp() else "*$pattern*".simpleGlobToRegExp()
|
||||
val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL)
|
||||
regex.containsMatchIn(value)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// e.g PatternSyntaxException
|
||||
Timber.e(e, "Failed to evaluate push condition")
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,27 +87,4 @@ class EventMatchCondition(
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun hasSpecialGlobChar(glob: String): Boolean {
|
||||
return glob.contains("*") || glob.contains("?")
|
||||
}
|
||||
|
||||
// Very simple glob to regexp converter
|
||||
private fun simpleGlobToRegExp(glob: String): String {
|
||||
var out = "" // "^"
|
||||
for (element in glob) {
|
||||
when (element) {
|
||||
'*' -> out += ".*"
|
||||
'?' -> out += '.'.toString()
|
||||
'.' -> out += "\\."
|
||||
'\\' -> out += "\\\\"
|
||||
else -> out += element
|
||||
}
|
||||
}
|
||||
out += "" // '$'.toString()
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.pushrules
|
||||
|
||||
enum class Kind(val value: String) {
|
||||
EventMatch("event_match"),
|
||||
ContainsDisplayName("contains_display_name"),
|
||||
RoomMemberCount("room_member_count"),
|
||||
SenderNotificationPermission("sender_notification_permission"),
|
||||
Unrecognised("");
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromString(value: String): Kind {
|
||||
return when (value) {
|
||||
"event_match" -> EventMatch
|
||||
"contains_display_name" -> ContainsDisplayName
|
||||
"room_member_count" -> RoomMemberCount
|
||||
"sender_notification_permission" -> SenderNotificationPermission
|
||||
else -> Unrecognised
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,15 +29,13 @@ class RoomMemberCountCondition(
|
|||
* If no prefix is present, this parameter defaults to ==.
|
||||
*/
|
||||
val iz: String
|
||||
) : Condition(Kind.RoomMemberCount) {
|
||||
) : Condition {
|
||||
|
||||
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveRoomMemberCountCondition(event, this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "Room member count is $iz"
|
||||
}
|
||||
override fun technicalDescription() = "Room member count is $iz"
|
||||
|
||||
internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
|
||||
// sanity checks
|
||||
|
|
|
@ -45,4 +45,6 @@ object RuleIds {
|
|||
|
||||
// Not documented
|
||||
const val RULE_ID_FALLBACK = ".m.rule.fallback"
|
||||
|
||||
const val RULE_ID_REACTION = ".m.rule.reaction"
|
||||
}
|
||||
|
|
|
@ -28,15 +28,13 @@ class SenderNotificationPermissionCondition(
|
|||
* type from the notifications object in the power level event content.
|
||||
*/
|
||||
val key: String
|
||||
) : Condition(Kind.SenderNotificationPermission) {
|
||||
) : Condition {
|
||||
|
||||
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "User power level <$key>"
|
||||
}
|
||||
override fun technicalDescription() = "User power level <$key>"
|
||||
|
||||
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
|
||||
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
|
||||
|
|
|
@ -21,7 +21,9 @@ import com.squareup.moshi.JsonClass
|
|||
import org.matrix.android.sdk.api.pushrules.Condition
|
||||
import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition
|
||||
import org.matrix.android.sdk.api.pushrules.EventMatchCondition
|
||||
import org.matrix.android.sdk.api.pushrules.Kind
|
||||
import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition
|
||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||
import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -58,20 +60,20 @@ data class PushCondition(
|
|||
val iz: String? = null
|
||||
) {
|
||||
|
||||
fun asExecutableCondition(): Condition? {
|
||||
return when (Condition.Kind.fromString(kind)) {
|
||||
Condition.Kind.EventMatch -> {
|
||||
fun asExecutableCondition(rule: PushRule): Condition? {
|
||||
return when (Kind.fromString(kind)) {
|
||||
Kind.EventMatch -> {
|
||||
if (key != null && pattern != null) {
|
||||
EventMatchCondition(key, pattern)
|
||||
EventMatchCondition(key, pattern, rule.ruleId == RuleIds.RULE_ID_CONTAIN_USER_NAME)
|
||||
} else {
|
||||
Timber.e("Malformed Event match condition")
|
||||
null
|
||||
}
|
||||
}
|
||||
Condition.Kind.ContainsDisplayName -> {
|
||||
Kind.ContainsDisplayName -> {
|
||||
ContainsDisplayNameCondition()
|
||||
}
|
||||
Condition.Kind.RoomMemberCount -> {
|
||||
Kind.RoomMemberCount -> {
|
||||
if (iz.isNullOrEmpty()) {
|
||||
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
|
||||
null
|
||||
|
@ -79,7 +81,7 @@ data class PushCondition(
|
|||
RoomMemberCountCondition(iz)
|
||||
}
|
||||
}
|
||||
Condition.Kind.SenderNotificationPermission -> {
|
||||
Kind.SenderNotificationPermission -> {
|
||||
if (key == null) {
|
||||
Timber.e("Malformed Sender Notification Permission condition")
|
||||
null
|
||||
|
@ -87,7 +89,7 @@ data class PushCondition(
|
|||
SenderNotificationPermissionCondition(key)
|
||||
}
|
||||
}
|
||||
Condition.Kind.Unrecognised -> {
|
||||
Kind.Unrecognised -> {
|
||||
Timber.e("Unknown kind $kind")
|
||||
null
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ data class PushRule(
|
|||
* Add the default notification sound.
|
||||
*/
|
||||
fun setNotificationSound(): PushRule {
|
||||
return setNotificationSound(ACTION_VALUE_DEFAULT)
|
||||
return setNotificationSound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT)
|
||||
}
|
||||
|
||||
fun getNotificationSound(): String? {
|
||||
|
@ -109,13 +109,13 @@ data class PushRule(
|
|||
fun setNotify(notify: Boolean): PushRule {
|
||||
val mutableActions = actions.toMutableList()
|
||||
|
||||
mutableActions.remove(ACTION_DONT_NOTIFY)
|
||||
mutableActions.remove(ACTION_NOTIFY)
|
||||
mutableActions.remove(Action.ACTION_DONT_NOTIFY)
|
||||
mutableActions.remove(Action.ACTION_NOTIFY)
|
||||
|
||||
if (notify) {
|
||||
mutableActions.add(ACTION_NOTIFY)
|
||||
mutableActions.add(Action.ACTION_NOTIFY)
|
||||
} else {
|
||||
mutableActions.add(ACTION_DONT_NOTIFY)
|
||||
mutableActions.add(Action.ACTION_DONT_NOTIFY)
|
||||
}
|
||||
|
||||
return copy(actions = mutableActions)
|
||||
|
@ -126,51 +126,12 @@ data class PushRule(
|
|||
*
|
||||
* @return true if the rule should play sound
|
||||
*/
|
||||
fun shouldNotify() = actions.contains(ACTION_NOTIFY)
|
||||
fun shouldNotify() = actions.contains(Action.ACTION_NOTIFY)
|
||||
|
||||
/**
|
||||
* Return true if the rule should not highlight the event.
|
||||
*
|
||||
* @return true if the rule should not play sound
|
||||
*/
|
||||
fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY)
|
||||
|
||||
companion object {
|
||||
/* ==========================================================================================
|
||||
* Rule id
|
||||
* ========================================================================================== */
|
||||
|
||||
const val RULE_ID_DISABLE_ALL = ".m.rule.master"
|
||||
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
|
||||
const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name"
|
||||
const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"
|
||||
const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me"
|
||||
const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event"
|
||||
const val RULE_ID_CALL = ".m.rule.call"
|
||||
const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices"
|
||||
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"
|
||||
const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif"
|
||||
const val RULE_ID_TOMBSTONE = ".m.rule.tombstone"
|
||||
const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one"
|
||||
const val RULE_ID_E2E_GROUP = ".m.rule.encrypted"
|
||||
const val RULE_ID_REACTION = ".m.rule.reaction"
|
||||
const val RULE_ID_FALLBACK = ".m.rule.fallback"
|
||||
|
||||
/* ==========================================================================================
|
||||
* Actions
|
||||
* ========================================================================================== */
|
||||
|
||||
const val ACTION_NOTIFY = "notify"
|
||||
const val ACTION_DONT_NOTIFY = "dont_notify"
|
||||
const val ACTION_COALESCE = "coalesce"
|
||||
|
||||
const val ACTION_SET_TWEAK_SOUND_VALUE = "sound"
|
||||
const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight"
|
||||
|
||||
const val ACTION_PARAMETER_SET_TWEAK = "set_tweak"
|
||||
const val ACTION_PARAMETER_VALUE = "value"
|
||||
|
||||
const val ACTION_VALUE_DEFAULT = "default"
|
||||
const val ACTION_VALUE_RING = "ring"
|
||||
}
|
||||
fun shouldNotNotify() = actions.contains(Action.ACTION_DONT_NOTIFY)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.pushrules.rest
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
||||
|
||||
/**
|
||||
|
@ -51,7 +52,7 @@ data class RuleSet(
|
|||
var result: PushRuleAndKind? = null
|
||||
// sanity check
|
||||
if (null != ruleId) {
|
||||
if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) {
|
||||
if (RuleIds.RULE_ID_CONTAIN_USER_NAME == ruleId) {
|
||||
result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) }
|
||||
} else {
|
||||
// assume that the ruleId is unique.
|
||||
|
|
|
@ -34,4 +34,6 @@ interface CallSignalingService {
|
|||
fun removeCallListener(listener: CallsListener)
|
||||
|
||||
fun getCallWithId(callId: String) : MxCall?
|
||||
|
||||
fun isThereAnyActiveCall(): Boolean
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ interface ContentUploadStateTracker {
|
|||
object Idle : State()
|
||||
object EncryptingThumbnail : State()
|
||||
data class UploadingThumbnail(val current: Long, val total: Long) : State()
|
||||
object Encrypting : State()
|
||||
data class Encrypting(val current: Long, val total: Long) : State()
|
||||
data class Uploading(val current: Long, val total: Long) : State()
|
||||
object Success : State()
|
||||
data class Failure(val throwable: Throwable) : State()
|
||||
|
|
|
@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Event.isAudioMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_AUDIO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isFileMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
|
@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean {
|
|||
else -> false
|
||||
}
|
||||
}
|
||||
fun Event.isAttachmentMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||
return if (isEncrypted()) {
|
||||
|
|
|
@ -25,7 +25,12 @@ import android.net.Uri
|
|||
*/
|
||||
sealed class PermalinkData {
|
||||
|
||||
data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
|
||||
data class RoomLink(
|
||||
val roomIdOrAlias: String,
|
||||
val isRoomAlias: Boolean,
|
||||
val eventId: String?,
|
||||
val viaParameters: List<String>
|
||||
) : PermalinkData()
|
||||
|
||||
data class UserLink(val userId: String) : PermalinkData()
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.matrix.android.sdk.api.session.permalinks
|
||||
|
||||
import android.net.Uri
|
||||
import android.net.UrlQuerySanitizer
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
|
||||
/**
|
||||
|
@ -40,14 +41,13 @@ object PermalinkParser {
|
|||
if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
|
||||
val fragment = uri.fragment
|
||||
if (fragment.isNullOrEmpty()) {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
|
||||
val indexOfQuery = fragment.indexOf("?")
|
||||
val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment
|
||||
val viaQueryParameters = fragment.getViaParameters()
|
||||
|
||||
// we are limiting to 2 params
|
||||
val params = safeFragment
|
||||
|
@ -65,17 +65,29 @@ object PermalinkParser {
|
|||
PermalinkData.RoomLink(
|
||||
roomIdOrAlias = identifier,
|
||||
isRoomAlias = false,
|
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
|
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
|
||||
viaParameters = viaQueryParameters
|
||||
)
|
||||
}
|
||||
MatrixPatterns.isRoomAlias(identifier) -> {
|
||||
PermalinkData.RoomLink(
|
||||
roomIdOrAlias = identifier,
|
||||
isRoomAlias = true,
|
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
|
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
|
||||
viaParameters = viaQueryParameters
|
||||
)
|
||||
}
|
||||
else -> PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.getViaParameters(): List<String> {
|
||||
return UrlQuerySanitizer(this)
|
||||
.parameterList
|
||||
.filter {
|
||||
it.mParameter == "via"
|
||||
}.map {
|
||||
it.mValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,4 +83,43 @@ interface ProfileService {
|
|||
* @param refreshData set to true to fetch data from the homeserver
|
||||
*/
|
||||
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
|
||||
|
||||
/**
|
||||
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
|
||||
*/
|
||||
fun getPendingThreePids(): List<ThreePid>
|
||||
|
||||
/**
|
||||
* Get the pending 3Pids Live
|
||||
*/
|
||||
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
|
||||
|
||||
/**
|
||||
* Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list.
|
||||
*/
|
||||
fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Validate a code received by text message
|
||||
*/
|
||||
fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
|
||||
*/
|
||||
fun finalizeAddingThreePid(threePid: ThreePid,
|
||||
uiaSession: String?,
|
||||
accountPassword: String?,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Cancel adding a threepid. It will remove locally stored data about this ThreePid
|
||||
*/
|
||||
fun cancelAddingThreePid(threePid: ThreePid,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Remove a 3Pid from the Matrix account.
|
||||
*/
|
||||
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
|
|
@ -110,13 +110,13 @@ interface SendService {
|
|||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
|
||||
fun resendTextMessage(localEcho: TimelineEvent): Cancelable
|
||||
|
||||
/**
|
||||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
|
||||
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
|
||||
|
||||
/**
|
||||
* Remove this failed message from the timeline
|
||||
|
@ -124,8 +124,16 @@ interface SendService {
|
|||
*/
|
||||
fun deleteFailedEcho(localEcho: TimelineEvent)
|
||||
|
||||
/**
|
||||
* Delete all the events in one of the sending states
|
||||
*/
|
||||
fun clearSendingQueue()
|
||||
|
||||
/**
|
||||
* Cancel sending a specific event. It has to be in one of the sending states
|
||||
*/
|
||||
fun cancelSend(eventId: String)
|
||||
|
||||
/**
|
||||
* Resend all failed messages one by one (and keep order)
|
||||
*/
|
||||
|
|
|
@ -37,7 +37,8 @@ enum class SendState {
|
|||
internal companion object {
|
||||
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
|
||||
val IS_SENT_STATES = listOf(SENT, SYNCED)
|
||||
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
|
||||
val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING)
|
||||
val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT
|
||||
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
|
||||
}
|
||||
|
||||
|
@ -45,5 +46,7 @@ enum class SendState {
|
|||
|
||||
fun hasFailed() = HAS_FAILED_STATES.contains(this)
|
||||
|
||||
fun isInProgress() = IS_PROGRESSING_STATES.contains(this)
|
||||
|
||||
fun isSending() = IS_SENDING_STATES.contains(this)
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ internal data class AddThreePidRegistrationParams(
|
|||
* This parameter is ignored when the homeserver handles 3PID verification.
|
||||
*/
|
||||
@Json(name = "id_server")
|
||||
val id_server: String? = null,
|
||||
val idServer: String? = null,
|
||||
|
||||
/* ==========================================================================================
|
||||
* For emails
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.auth.registration
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
|
@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
|
|||
import org.matrix.android.sdk.internal.network.RetrofitFactory
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* This class execute the registration request and is responsible to keep the session of interactive authentication
|
||||
|
@ -193,7 +193,7 @@ internal class DefaultRegistrationWizard(
|
|||
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
|
||||
val validationBody = ValidationCodeBody(
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
|
|
|
@ -44,5 +44,6 @@ internal data class RegistrationParams(
|
|||
|
||||
// Temporary flag to notify the server that we support msisdn flow. Used to prevent old app
|
||||
// versions to end up in fallback because the HS returns the msisdn flow which they don't support
|
||||
val x_show_msisdn: Boolean? = null
|
||||
@Json(name = "x_show_msisdn")
|
||||
val xShowMsisdn: Boolean? = null
|
||||
)
|
||||
|
|
|
@ -145,7 +145,7 @@ object MXMegolmExportEncryption {
|
|||
*/
|
||||
@Throws(Exception::class)
|
||||
@JvmOverloads
|
||||
fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
|
||||
fun encryptMegolmKeyFile(data: String, password: String, kdfRounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
|
||||
if (password.isEmpty()) {
|
||||
throw Exception("Empty password is not supported")
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ object MXMegolmExportEncryption {
|
|||
// of a single bit of salt is a price we have to pay.
|
||||
iv[9] = iv[9] and 0x7f
|
||||
|
||||
val deriveKey = deriveKeys(salt, kdf_rounds, password)
|
||||
val deriveKey = deriveKeys(salt, kdfRounds, password)
|
||||
|
||||
val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
|
||||
|
@ -188,10 +188,10 @@ object MXMegolmExportEncryption {
|
|||
System.arraycopy(iv, 0, resultBuffer, idx, iv.size)
|
||||
idx += iv.size
|
||||
|
||||
resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdf_rounds and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdfRounds shr 24 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdfRounds shr 16 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdfRounds shr 8 and 0xff).toByte()
|
||||
resultBuffer[idx++] = (kdfRounds and 0xff).toByte()
|
||||
|
||||
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
|
||||
idx += cipherArray.size
|
||||
|
@ -320,26 +320,26 @@ object MXMegolmExportEncryption {
|
|||
|
||||
// 512 bits key length
|
||||
val key = ByteArray(64)
|
||||
val Uc = ByteArray(64)
|
||||
val uc = ByteArray(64)
|
||||
|
||||
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
||||
prf.update(salt)
|
||||
val int32BE = ByteArray(4) { 0.toByte() }
|
||||
int32BE[3] = 1.toByte()
|
||||
prf.update(int32BE)
|
||||
prf.doFinal(Uc, 0)
|
||||
prf.doFinal(uc, 0)
|
||||
|
||||
// copy to the key
|
||||
System.arraycopy(Uc, 0, key, 0, Uc.size)
|
||||
System.arraycopy(uc, 0, key, 0, uc.size)
|
||||
|
||||
for (index in 2..iterations) {
|
||||
// Uc = PRF(Password, Uc-1)
|
||||
prf.update(Uc)
|
||||
prf.doFinal(Uc, 0)
|
||||
prf.update(uc)
|
||||
prf.doFinal(uc, 0)
|
||||
|
||||
// F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc
|
||||
for (byteIndex in Uc.indices) {
|
||||
key[byteIndex] = key[byteIndex] xor Uc[byteIndex]
|
||||
for (byteIndex in uc.indices) {
|
||||
key[byteIndex] = key[byteIndex] xor uc[byteIndex]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ internal class MXOlmDecryption(
|
|||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
|
||||
}
|
||||
|
||||
val recipientKeys = olmPayloadContent.recipient_keys ?: run {
|
||||
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
|
||||
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
||||
" property; cannot prevent unknown-key attack")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
|
@ -129,10 +129,10 @@ internal class MXOlmDecryption(
|
|||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
|
||||
}
|
||||
|
||||
if (olmPayloadContent.room_id != event.roomId) {
|
||||
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}")
|
||||
if (olmPayloadContent.roomId != event.roomId) {
|
||||
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
|
||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id))
|
||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
|
||||
}
|
||||
|
||||
val keys = olmPayloadContent.keys ?: run {
|
||||
|
|
|
@ -20,10 +20,14 @@ package org.matrix.android.sdk.internal.crypto.attachments
|
|||
import android.util.Base64
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
|
||||
import org.matrix.android.sdk.internal.util.base64ToBase64Url
|
||||
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
|
||||
import org.matrix.android.sdk.internal.util.base64UrlToBase64
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
|
@ -36,8 +40,121 @@ internal object MXEncryptedAttachments {
|
|||
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
||||
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
|
||||
|
||||
fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo {
|
||||
val t0 = System.currentTimeMillis()
|
||||
val secureRandom = SecureRandom()
|
||||
val initVectorBytes = ByteArray(16) { 0.toByte() }
|
||||
|
||||
val ivRandomPart = ByteArray(8)
|
||||
secureRandom.nextBytes(ivRandomPart)
|
||||
|
||||
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
|
||||
|
||||
val key = ByteArray(32)
|
||||
secureRandom.nextBytes(key)
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
|
||||
outputFile.outputStream().use { outputStream ->
|
||||
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
|
||||
val data = ByteArray(CRYPTO_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var encodedBytes: ByteArray
|
||||
clearStream.use { inputStream ->
|
||||
val estimatedSize = inputStream.available()
|
||||
progress.invoke(0, estimatedSize)
|
||||
read = inputStream.read(data)
|
||||
var totalRead = read
|
||||
while (read != -1) {
|
||||
progress.invoke(totalRead, estimatedSize)
|
||||
encodedBytes = encryptCipher.update(data, 0, read)
|
||||
messageDigest.update(encodedBytes, 0, encodedBytes.size)
|
||||
outputStream.write(encodedBytes)
|
||||
read = inputStream.read(data)
|
||||
totalRead += read
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt the latest chunk
|
||||
encodedBytes = encryptCipher.doFinal()
|
||||
messageDigest.update(encodedBytes, 0, encodedBytes.size)
|
||||
outputStream.write(encodedBytes)
|
||||
}
|
||||
|
||||
return EncryptedFileInfo(
|
||||
url = null,
|
||||
mimetype = mimetype,
|
||||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
ext = true,
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
|
||||
),
|
||||
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
|
||||
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
|
||||
v = "v2"
|
||||
)
|
||||
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
|
||||
}
|
||||
|
||||
// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair<DigestInputStream, EncryptedFileInfo> {
|
||||
// val secureRandom = SecureRandom()
|
||||
//
|
||||
// // generate a random iv key
|
||||
// // Half of the IV is random, the lower order bits are zeroed
|
||||
// // such that the counter never wraps.
|
||||
// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
|
||||
// val initVectorBytes = ByteArray(16) { 0.toByte() }
|
||||
//
|
||||
// val ivRandomPart = ByteArray(8)
|
||||
// secureRandom.nextBytes(ivRandomPart)
|
||||
//
|
||||
// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
|
||||
//
|
||||
// val key = ByteArray(32)
|
||||
// secureRandom.nextBytes(key)
|
||||
//
|
||||
// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
// val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
//
|
||||
// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher)
|
||||
//
|
||||
// // Could it be possible to get the digest on the fly instead of
|
||||
// val info = EncryptedFileInfo(
|
||||
// url = null,
|
||||
// mimetype = mimetype,
|
||||
// key = EncryptedFileKey(
|
||||
// alg = "A256CTR",
|
||||
// ext = true,
|
||||
// key_ops = listOf("encrypt", "decrypt"),
|
||||
// kty = "oct",
|
||||
// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
|
||||
// ),
|
||||
// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
|
||||
// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
|
||||
// v = "v2"
|
||||
// )
|
||||
//
|
||||
// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
// return DigestInputStream(cipherInputStream, messageDigest) to info
|
||||
// }
|
||||
//
|
||||
// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo {
|
||||
// return info.copy(
|
||||
// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT)))
|
||||
// )
|
||||
// }
|
||||
|
||||
/***
|
||||
* Encrypt an attachment stream.
|
||||
* DO NOT USE for big files, it will load all in memory
|
||||
* @param attachmentStream the attachment stream. Will be closed after this method call.
|
||||
* @param mimetype the mime type
|
||||
* @return the encryption file info
|
||||
|
@ -60,14 +177,14 @@ internal object MXEncryptedAttachments {
|
|||
val key = ByteArray(32)
|
||||
secureRandom.nextBytes(key)
|
||||
|
||||
ByteArrayOutputStream().use { outputStream ->
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
byteArrayOutputStream.use { outputStream ->
|
||||
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
|
||||
val data = ByteArray(CRYPTO_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var encodedBytes: ByteArray
|
||||
|
@ -86,44 +203,26 @@ internal object MXEncryptedAttachments {
|
|||
encodedBytes = encryptCipher.doFinal()
|
||||
messageDigest.update(encodedBytes, 0, encodedBytes.size)
|
||||
outputStream.write(encodedBytes)
|
||||
|
||||
return EncryptionResult(
|
||||
encryptedFileInfo = EncryptedFileInfo(
|
||||
url = null,
|
||||
mimetype = mimetype,
|
||||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
ext = true,
|
||||
key_ops = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
|
||||
),
|
||||
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
|
||||
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
|
||||
v = "v2"
|
||||
),
|
||||
encryptedByteArray = outputStream.toByteArray()
|
||||
)
|
||||
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an attachment
|
||||
*
|
||||
* @param attachmentStream the attachment stream. Will be closed after this method call.
|
||||
* @param encryptedFileInfo the encryption file info
|
||||
* @return the decrypted attachment stream
|
||||
*/
|
||||
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? {
|
||||
if (encryptedFileInfo?.isValid() != true) {
|
||||
Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields")
|
||||
return null
|
||||
}
|
||||
|
||||
val elementToDecrypt = encryptedFileInfo.toElementToDecrypt()
|
||||
|
||||
return decryptAttachment(attachmentStream, elementToDecrypt)
|
||||
return EncryptionResult(
|
||||
encryptedFileInfo = EncryptedFileInfo(
|
||||
url = null,
|
||||
mimetype = mimetype,
|
||||
key = EncryptedFileKey(
|
||||
alg = "A256CTR",
|
||||
ext = true,
|
||||
keyOps = listOf("encrypt", "decrypt"),
|
||||
kty = "oct",
|
||||
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
|
||||
),
|
||||
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
|
||||
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
|
||||
v = "v2"
|
||||
),
|
||||
encryptedByteArray = byteArrayOutputStream.toByteArray()
|
||||
)
|
||||
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,84 +230,61 @@ internal object MXEncryptedAttachments {
|
|||
*
|
||||
* @param attachmentStream the attachment stream. Will be closed after this method call.
|
||||
* @param elementToDecrypt the elementToDecrypt info
|
||||
* @return the decrypted attachment stream
|
||||
* @param outputStream the outputStream where the decrypted attachment will be write.
|
||||
* @return true in case of success, false in case of error
|
||||
*/
|
||||
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? {
|
||||
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean {
|
||||
// sanity checks
|
||||
if (null == attachmentStream || elementToDecrypt == null) {
|
||||
Timber.e("## decryptAttachment() : null stream")
|
||||
return null
|
||||
return false
|
||||
}
|
||||
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
||||
ByteArrayOutputStream().use { outputStream ->
|
||||
try {
|
||||
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
|
||||
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
|
||||
try {
|
||||
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
|
||||
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
|
||||
|
||||
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
|
||||
var read: Int
|
||||
val data = ByteArray(CRYPTO_BUFFER_SIZE)
|
||||
var decodedBytes: ByteArray
|
||||
var read: Int
|
||||
val data = ByteArray(CRYPTO_BUFFER_SIZE)
|
||||
var decodedBytes: ByteArray
|
||||
|
||||
attachmentStream.use { inputStream ->
|
||||
attachmentStream.use { inputStream ->
|
||||
read = inputStream.read(data)
|
||||
while (read != -1) {
|
||||
messageDigest.update(data, 0, read)
|
||||
decodedBytes = decryptCipher.update(data, 0, read)
|
||||
outputStream.write(decodedBytes)
|
||||
read = inputStream.read(data)
|
||||
while (read != -1) {
|
||||
messageDigest.update(data, 0, read)
|
||||
decodedBytes = decryptCipher.update(data, 0, read)
|
||||
outputStream.write(decodedBytes)
|
||||
read = inputStream.read(data)
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt the last chunk
|
||||
decodedBytes = decryptCipher.doFinal()
|
||||
outputStream.write(decodedBytes)
|
||||
|
||||
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
|
||||
|
||||
if (elementToDecrypt.sha256 != currentDigestValue) {
|
||||
Timber.e("## decryptAttachment() : Digest value mismatch")
|
||||
return null
|
||||
}
|
||||
|
||||
return ByteArrayInputStream(outputStream.toByteArray())
|
||||
.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## decryptAttachment() failed: OOM")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptAttachment() failed")
|
||||
}
|
||||
|
||||
// decrypt the last chunk
|
||||
decodedBytes = decryptCipher.doFinal()
|
||||
outputStream.write(decodedBytes)
|
||||
|
||||
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
|
||||
|
||||
if (elementToDecrypt.sha256 != currentDigestValue) {
|
||||
Timber.e("## decryptAttachment() : Digest value mismatch")
|
||||
return false
|
||||
}
|
||||
|
||||
return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## decryptAttachment() failed: OOM")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptAttachment() failed")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL conversion methods
|
||||
*/
|
||||
|
||||
private fun base64UrlToBase64(base64Url: String): String {
|
||||
return base64Url.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
}
|
||||
|
||||
internal fun base64ToBase64Url(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("\\+".toRegex(), "-")
|
||||
.replace('/', '_')
|
||||
.replace("=", "")
|
||||
}
|
||||
|
||||
private fun base64ToUnpaddedBase64(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("=", "")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 org.matrix.android.sdk.internal.crypto.attachments
|
||||
|
||||
import android.util.Base64
|
||||
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
|
||||
import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
class MatrixDigestCheckInputStream(
|
||||
inputStream: InputStream?,
|
||||
private val expectedDigest: String
|
||||
) : FilterInputStream(inputStream) {
|
||||
|
||||
private val digest = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(): Int {
|
||||
val b = `in`.read()
|
||||
if (b >= 0) {
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
if (b == -1) {
|
||||
ensureDigest()
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(
|
||||
b: ByteArray,
|
||||
off: Int,
|
||||
len: Int): Int {
|
||||
val n = `in`.read(b, off, len)
|
||||
if (n > 0) {
|
||||
digest.update(b, off, n)
|
||||
}
|
||||
|
||||
if (n == -1) {
|
||||
ensureDigest()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun ensureDigest() {
|
||||
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT))
|
||||
if (currentDigestValue != expectedDigest) {
|
||||
throw IOException("Bad digest")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.model.event
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
|
@ -27,27 +28,32 @@ data class OlmPayloadContent(
|
|||
/**
|
||||
* The room id
|
||||
*/
|
||||
var room_id: String? = null,
|
||||
@Json(name = "room_id")
|
||||
val roomId: String? = null,
|
||||
|
||||
/**
|
||||
* The sender
|
||||
*/
|
||||
var sender: String? = null,
|
||||
@Json(name = "sender")
|
||||
val sender: String? = null,
|
||||
|
||||
/**
|
||||
* The recipient
|
||||
*/
|
||||
var recipient: String? = null,
|
||||
@Json(name = "recipient")
|
||||
val recipient: String? = null,
|
||||
|
||||
/**
|
||||
* the recipient keys
|
||||
*/
|
||||
var recipient_keys: Map<String, String>? = null,
|
||||
@Json(name = "recipient_keys")
|
||||
val recipientKeys: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The keys
|
||||
*/
|
||||
var keys: Map<String, String>? = null
|
||||
@Json(name = "keys")
|
||||
val keys: Map<String, String>? = null
|
||||
) {
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this)
|
||||
|
|
|
@ -31,7 +31,7 @@ data class DeviceInfo(
|
|||
* The owner user id (not documented and useless but the homeserver sent it. You should not need it)
|
||||
*/
|
||||
@Json(name = "user_id")
|
||||
val user_id: String? = null,
|
||||
val userId: String? = null,
|
||||
|
||||
/**
|
||||
* The device id
|
||||
|
|
|
@ -37,7 +37,7 @@ data class EncryptedFileKey(
|
|||
* Required. Key operations. Must at least contain "encrypt" and "decrypt".
|
||||
*/
|
||||
@Json(name = "key_ops")
|
||||
val key_ops: List<String>? = null,
|
||||
val keyOps: List<String>? = null,
|
||||
|
||||
/**
|
||||
* Required. Key type. Must be "oct".
|
||||
|
@ -63,7 +63,7 @@ data class EncryptedFileKey(
|
|||
return false
|
||||
}
|
||||
|
||||
if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) {
|
||||
if (keyOps?.contains("encrypt") != true || !keyOps.contains("decrypt")) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.util.Base64
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
@ -96,7 +95,7 @@ fun <T> deserializeFromRealm(string: String?): T? {
|
|||
}
|
||||
val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT)
|
||||
|
||||
val bais = ByteArrayInputStream(decodedB64)
|
||||
val bais = decodedB64.inputStream()
|
||||
val gzis = GZIPInputStream(bais)
|
||||
val ois = SafeObjectInputStream(gzis)
|
||||
return ois.use {
|
||||
|
|
|
@ -28,7 +28,7 @@ import java.io.ObjectStreamClass
|
|||
*
|
||||
* Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes
|
||||
*/
|
||||
internal class SafeObjectInputStream(`in`: InputStream) : ObjectInputStream(`in`) {
|
||||
internal class SafeObjectInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) {
|
||||
|
||||
init {
|
||||
enableResolveObject(true)
|
||||
|
|
|
@ -175,8 +175,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
?.unpaddedBase64PublicKey
|
||||
?.let { masterPublicKey ->
|
||||
val crossSigningKeyId = "ed25519:$masterPublicKey"
|
||||
macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString ->
|
||||
keyMap[crossSigningKeyId] = MSKMacString
|
||||
macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { mskMacString ->
|
||||
keyMap[crossSigningKeyId] = mskMacString
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,14 +55,14 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
|
|||
31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot)
|
||||
32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat)
|
||||
33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses)
|
||||
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench)
|
||||
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_spanner, R.drawable.ic_verification_spanner)
|
||||
35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa)
|
||||
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up)
|
||||
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbs_up, R.drawable.ic_verification_thumbs_up)
|
||||
37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella)
|
||||
38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass)
|
||||
39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock, R.drawable.ic_verification_clock)
|
||||
40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift)
|
||||
41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb)
|
||||
41 -> EmojiRepresentation("💡", R.string.verification_emoji_light_bulb, R.drawable.ic_verification_light_bulb)
|
||||
42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book)
|
||||
43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil)
|
||||
44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip)
|
||||
|
@ -74,7 +74,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
|
|||
50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag)
|
||||
51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train)
|
||||
52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle)
|
||||
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane)
|
||||
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_aeroplane, R.drawable.ic_verification_aeroplane)
|
||||
54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket)
|
||||
55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy)
|
||||
56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball, R.drawable.ic_verification_ball)
|
||||
|
@ -82,7 +82,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
|
|||
58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet)
|
||||
59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell)
|
||||
60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor)
|
||||
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone)
|
||||
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphones, R.drawable.ic_verification_headphones)
|
||||
62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder)
|
||||
/* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin)
|
||||
}
|
||||
|
|
|
@ -20,18 +20,24 @@ package org.matrix.android.sdk.internal.database
|
|||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 4L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
|
||||
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
if (oldVersion <= 1) migrateTo2(realm)
|
||||
if (oldVersion <= 2) migrateTo3(realm)
|
||||
if (oldVersion <= 3) migrateTo4(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -63,4 +69,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateTo4(realm: DynamicRealm) {
|
||||
Timber.d("Step 3 -> 4")
|
||||
realm.schema.create("PendingThreePidEntity")
|
||||
.addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
|
||||
.setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
|
||||
.addField(PendingThreePidEntityFields.EMAIL, String::class.java)
|
||||
.addField(PendingThreePidEntityFields.MSISDN, String::class.java)
|
||||
.addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
|
||||
.addField(PendingThreePidEntityFields.SID, String::class.java)
|
||||
.setRequired(PendingThreePidEntityFields.SID, true)
|
||||
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,14 @@ package org.matrix.android.sdk.internal.database
|
|||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
|
||||
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.UserMd5
|
||||
import org.matrix.android.sdk.internal.session.SessionModule
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -46,20 +47,16 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
val migration: RealmSessionStoreMigration,
|
||||
context: Context) {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 3L
|
||||
}
|
||||
|
||||
// Keep legacy preferences name for compatibility reason
|
||||
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
|
||||
|
||||
fun create(): RealmConfiguration {
|
||||
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
|
||||
if (shouldClearRealm) {
|
||||
Timber.v("************************************************************")
|
||||
Timber.v("The realm file session was corrupted and couldn't be loaded.")
|
||||
Timber.v("The file has been deleted to recover.")
|
||||
Timber.v("************************************************************")
|
||||
Timber.e("************************************************************")
|
||||
Timber.e("The realm file session was corrupted and couldn't be loaded.")
|
||||
Timber.e("The file has been deleted to recover.")
|
||||
Timber.e("************************************************************")
|
||||
deleteRealmFiles()
|
||||
}
|
||||
sharedPreferences.edit {
|
||||
|
@ -74,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||
}
|
||||
.modules(SessionRealmModule())
|
||||
.schemaVersion(SESSION_STORE_SCHEMA_VERSION)
|
||||
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
|
||||
.migration(migration)
|
||||
.build()
|
||||
|
||||
|
@ -90,6 +87,11 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
|
||||
// Delete all the realm files of the session
|
||||
private fun deleteRealmFiles() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.e("No op because it is a debug build")
|
||||
return
|
||||
}
|
||||
|
||||
listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file ->
|
||||
try {
|
||||
File(directory, file).deleteRecursively()
|
||||
|
|
|
@ -17,12 +17,12 @@
|
|||
package org.matrix.android.sdk.internal.database.mapper
|
||||
|
||||
import com.squareup.moshi.Types
|
||||
import org.matrix.android.sdk.api.pushrules.Condition
|
||||
import io.realm.RealmList
|
||||
import org.matrix.android.sdk.api.pushrules.Kind
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushCondition
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.internal.database.model.PushRuleEntity
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import io.realm.RealmList
|
||||
import timber.log.Timber
|
||||
|
||||
internal object PushRulesMapper {
|
||||
|
@ -39,7 +39,7 @@ internal object PushRulesMapper {
|
|||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
|
||||
PushCondition(Kind.EventMatch.value, "content.body", pushrule.pattern)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ internal object PushRulesMapper {
|
|||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
|
||||
PushCondition(Kind.EventMatch.value, "room_id", pushrule.ruleId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ internal object PushRulesMapper {
|
|||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
|
||||
PushCondition(Kind.EventMatch.value, "user_id", pushrule.ruleId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
/**
|
||||
* This class is used to store pending threePid data, when user wants to add a threePid to his account
|
||||
*/
|
||||
internal open class PendingThreePidEntity(
|
||||
var email: String? = null,
|
||||
var msisdn: String? = null,
|
||||
var clientSecret: String = "",
|
||||
var sendAttempt: Int = 0,
|
||||
var sid: String = "",
|
||||
var submitUrl: String? = null
|
||||
) : RealmObject()
|
|
@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
|
|||
RoomSummaryEntity::class,
|
||||
RoomTagEntity::class,
|
||||
SyncEntity::class,
|
||||
PendingThreePidEntity::class,
|
||||
UserEntity::class,
|
||||
IgnoredUserEntity::class,
|
||||
BreadcrumbsEntity::class,
|
||||
|
|
|
@ -24,6 +24,7 @@ import okio.BufferedSink
|
|||
import okio.ForwardingSink
|
||||
import okio.Sink
|
||||
import okio.buffer
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import java.io.IOException
|
||||
|
||||
internal class ProgressRequestBody(private val delegate: RequestBody,
|
||||
|
@ -35,15 +36,13 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
|
|||
return delegate.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
try {
|
||||
return delegate.contentLength()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
override fun isOneShot() = delegate.isOneShot()
|
||||
|
||||
return -1
|
||||
}
|
||||
override fun isDuplex() = delegate.isDuplex()
|
||||
|
||||
val length = tryThis { delegate.contentLength() } ?: -1
|
||||
|
||||
override fun contentLength() = length
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
|
|
|
@ -19,13 +19,12 @@ package org.matrix.android.sdk.internal.network
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
|
||||
|
@ -50,7 +49,6 @@ internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
|
|||
return okHttpClient.get().newCall(request)
|
||||
}
|
||||
})
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(UnitConverterFactory)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 org.matrix.android.sdk.internal.network.parsing
|
||||
|
||||
import androidx.annotation.Nullable
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Type
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row.
|
||||
*/
|
||||
interface CheckNumberType {
|
||||
|
||||
companion object {
|
||||
val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory {
|
||||
@Nullable
|
||||
override fun create(type: Type, annotations: Set<Annotation?>?, moshi: Moshi): JsonAdapter<*>? {
|
||||
if (type !== Any::class.java) {
|
||||
return null
|
||||
}
|
||||
val delegate: JsonAdapter<Any> = moshi.nextAdapter(this, Any::class.java, emptySet())
|
||||
return object : JsonAdapter<Any?>() {
|
||||
@Nullable
|
||||
@Throws(IOException::class)
|
||||
override fun fromJson(reader: JsonReader): Any? {
|
||||
return if (reader.peek() !== JsonReader.Token.NUMBER) {
|
||||
delegate.fromJson(reader)
|
||||
} else {
|
||||
val numberAsString = reader.nextString()
|
||||
val decimal = BigDecimal(numberAsString)
|
||||
if (decimal.scale() <= 0) {
|
||||
decimal.intValueExact()
|
||||
} else {
|
||||
decimal.toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: Any?) {
|
||||
delegate.toJson(writer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor(
|
|||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||
|
||||
if (elementToDecrypt != null) {
|
||||
Timber.v("## decrypt file")
|
||||
val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt)
|
||||
Timber.v("## FileService: decrypt file")
|
||||
val decryptSuccess = MXEncryptedAttachments.decryptAttachment(
|
||||
source.inputStream(),
|
||||
elementToDecrypt,
|
||||
destFile.outputStream().buffered()
|
||||
)
|
||||
response.close()
|
||||
if (decryptedStream == null) {
|
||||
if (!decryptSuccess) {
|
||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||
} else {
|
||||
decryptedStream.use {
|
||||
writeToFile(decryptedStream, destFile)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeToFile(source.inputStream(), destFile)
|
||||
response.close()
|
||||
}
|
||||
} else {
|
||||
Timber.v("## FileService: cache hit for $url")
|
||||
}
|
||||
|
||||
Try.just(copyFile(destFile, downloadMode))
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 org.matrix.android.sdk.internal.session.call
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class ActiveCallHandler @Inject constructor() {
|
||||
|
||||
private val activeCallListLiveData: MutableLiveData<MutableList<MxCall>> by lazy {
|
||||
MutableLiveData<MutableList<MxCall>>(mutableListOf())
|
||||
}
|
||||
|
||||
fun addCall(call: MxCall) {
|
||||
activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { add(call) })
|
||||
}
|
||||
|
||||
fun removeCall(callId: String) {
|
||||
activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { removeAll { it.callId == callId } })
|
||||
}
|
||||
|
||||
fun getCallWithId(callId: String): MxCall? {
|
||||
return activeCallListLiveData.value?.find { it.callId == callId }
|
||||
}
|
||||
|
||||
fun getActiveCallsLiveData(): LiveData<MutableList<MxCall>> = activeCallListLiveData
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.call
|
||||
|
||||
import android.os.SystemClock
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||
|
@ -48,6 +49,7 @@ import javax.inject.Inject
|
|||
internal class DefaultCallSignalingService @Inject constructor(
|
||||
@UserId
|
||||
private val userId: String,
|
||||
private val activeCallHandler: ActiveCallHandler,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val roomEventSender: RoomEventSender,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
|
@ -56,13 +58,11 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
|
||||
private val callListeners = mutableSetOf<CallsListener>()
|
||||
|
||||
private val activeCalls = mutableListOf<MxCall>()
|
||||
|
||||
private val cachedTurnServerResponse = object {
|
||||
|
||||
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
|
||||
private val MIN_TTL = 60
|
||||
|
||||
private val now = { System.currentTimeMillis() / 1000 }
|
||||
private val now = { SystemClock.elapsedRealtime() / 1000 }
|
||||
|
||||
private var expiresAt: Long = 0
|
||||
|
||||
|
@ -96,7 +96,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
|
||||
return MxCallImpl(
|
||||
val call = MxCallImpl(
|
||||
callId = UUID.randomUUID().toString(),
|
||||
isOutgoing = true,
|
||||
roomId = roomId,
|
||||
|
@ -105,8 +105,9 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
isVideoCall = isVideoCall,
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
).also {
|
||||
activeCalls.add(it)
|
||||
)
|
||||
activeCallHandler.addCall(call).also {
|
||||
return call
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,8 +120,12 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getCallWithId(callId: String): MxCall? {
|
||||
Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}")
|
||||
return activeCalls.find { it.callId == callId }
|
||||
Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}")
|
||||
return activeCallHandler.getCallWithId(callId)
|
||||
}
|
||||
|
||||
override fun isThereAnyActiveCall(): Boolean {
|
||||
return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
internal fun onCallEvent(event: Event) {
|
||||
|
@ -151,6 +156,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
// Always ignore local echos of invite
|
||||
return
|
||||
}
|
||||
|
||||
event.getClearContent().toModel<CallInviteContent>()?.let { content ->
|
||||
val incomingCall = MxCallImpl(
|
||||
callId = content.callId ?: return@let,
|
||||
|
@ -162,7 +168,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
)
|
||||
activeCalls.add(incomingCall)
|
||||
activeCallHandler.addCall(incomingCall)
|
||||
onCallInvite(incomingCall, content)
|
||||
}
|
||||
}
|
||||
|
@ -184,8 +190,8 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
activeCallHandler.removeCall(content.callId)
|
||||
onCallHangup(content)
|
||||
activeCalls.removeAll { it.callId == content.callId }
|
||||
}
|
||||
}
|
||||
EventType.CALL_CANDIDATES -> {
|
||||
|
@ -194,7 +200,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
return
|
||||
}
|
||||
event.getClearContent().toModel<CallCandidatesContent>()?.let { content ->
|
||||
activeCalls.firstOrNull { it.callId == content.callId }?.let {
|
||||
activeCallHandler.getCallWithId(content.callId)?.let {
|
||||
onCallIceCandidate(it, content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
|||
updateState(key, progressData)
|
||||
}
|
||||
|
||||
internal fun setEncrypting(key: String) {
|
||||
val progressData = ContentUploadStateTracker.State.Encrypting
|
||||
internal fun setEncrypting(key: String, current: Long, total: Long) {
|
||||
val progressData = ContentUploadStateTracker.State.Encrypting(current, total)
|
||||
updateState(key, progressData)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,13 +23,16 @@ import com.squareup.moshi.Moshi
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okio.BufferedSink
|
||||
import okio.source
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.internal.di.Authenticated
|
||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||
|
@ -38,6 +41,7 @@ import org.matrix.android.sdk.internal.network.toFailure
|
|||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class FileUploader @Inject constructor(@Authenticated
|
||||
|
@ -54,7 +58,21 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
filename: String?,
|
||||
mimeType: String?,
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
|
||||
val uploadBody = object : RequestBody() {
|
||||
override fun contentLength() = file.length()
|
||||
|
||||
// Disable okhttp auto resend for 'large files'
|
||||
override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return mimeType?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
file.source().use { sink.writeAll(it) }
|
||||
}
|
||||
}
|
||||
|
||||
return upload(uploadBody, filename, progressListener)
|
||||
}
|
||||
|
||||
|
@ -70,14 +88,18 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
filename: String?,
|
||||
mimeType: String?,
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
inputStream.use {
|
||||
uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
|
||||
}
|
||||
val inputStream = withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
} ?: throw FileNotFoundException()
|
||||
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
workingFile.outputStream().use {
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
return uploadFile(workingFile, filename, mimeType, progressListener).also {
|
||||
tryThis { workingFile.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
||||
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
|
||||
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ImageCompressor @Inject constructor() {
|
||||
suspend fun compress(
|
||||
context: Context,
|
||||
imageFile: File,
|
||||
desiredWidth: Int,
|
||||
desiredHeight: Int,
|
||||
desiredQuality: Int = 80): File {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val compressedBitmap = BitmapFactory.Options().run {
|
||||
inJustDecodeBounds = true
|
||||
decodeBitmap(imageFile, this)
|
||||
inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight)
|
||||
inJustDecodeBounds = false
|
||||
decodeBitmap(imageFile, this)?.let {
|
||||
rotateBitmap(imageFile, it)
|
||||
}
|
||||
} ?: return@withContext imageFile
|
||||
|
||||
val destinationFile = createDestinationFile(context)
|
||||
|
||||
runCatching {
|
||||
destinationFile.outputStream().use {
|
||||
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext destinationFile
|
||||
}
|
||||
}
|
||||
|
||||
private fun rotateBitmap(file: File, bitmap: Bitmap): Bitmap {
|
||||
file.inputStream().use { inputStream ->
|
||||
try {
|
||||
ExifInterface(inputStream).let { exifInfo ->
|
||||
val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.preRotate(-90f)
|
||||
matrix.preScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.preRotate(90f)
|
||||
matrix.preScale(-1f, 1f)
|
||||
}
|
||||
else -> return bitmap
|
||||
}
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Cannot read orientation")
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
// https://developer.android.com/topic/performance/graphics/load-bitmap
|
||||
private fun calculateInSampleSize(width: Int, height: Int, desiredWidth: Int, desiredHeight: Int): Int {
|
||||
var inSampleSize = 1
|
||||
|
||||
if (width > desiredWidth || height > desiredHeight) {
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
|
||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
private fun decodeBitmap(file: File, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? {
|
||||
return try {
|
||||
file.inputStream().use { inputStream ->
|
||||
BitmapFactory.decodeStream(inputStream, null, options)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Cannot decode Bitmap")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDestinationFile(context: Context): File {
|
||||
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
}
|
||||
}
|
|
@ -22,8 +22,7 @@ import android.graphics.BitmapFactory
|
|||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import id.zelory.compressor.Compressor
|
||||
import id.zelory.compressor.constraint.default
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
|
@ -37,15 +36,13 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
|||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||
import org.matrix.android.sdk.internal.session.DefaultFileService
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -74,6 +71,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
@Inject lateinit var fileUploader: FileUploader
|
||||
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
|
||||
@Inject lateinit var fileService: DefaultFileService
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
@Inject lateinit var imageCompressor: ImageCompressor
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
|
@ -101,9 +100,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val attachment = params.attachment
|
||||
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
|
||||
if (allCancelled) {
|
||||
// there is no point in uploading the image!
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("## Send: Work cancelled by user") }
|
||||
}
|
||||
|
||||
var newImageAttributes: NewImageAttributes? = null
|
||||
val attachment = params.attachment
|
||||
val filesToDelete = mutableListOf<File>()
|
||||
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
||||
|
@ -115,124 +120,100 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
)
|
||||
)
|
||||
|
||||
inputStream.use {
|
||||
var uploadedThumbnailUrl: String? = null
|
||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||
|
||||
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
|
||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val contentUploadResponse = if (params.isEncrypted) {
|
||||
Timber.v("Encrypt thumbnail")
|
||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
"thumb_${attachment.name}",
|
||||
"application/octet-stream",
|
||||
thumbnailProgressListener)
|
||||
} else {
|
||||
fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||
"thumb_${attachment.name}",
|
||||
thumbnailData.mimeType,
|
||||
thumbnailProgressListener)
|
||||
}
|
||||
|
||||
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Thumbnail update failed")
|
||||
}
|
||||
}
|
||||
|
||||
val progressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
notifyTracker(params) {
|
||||
if (isStopped) {
|
||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||
} else {
|
||||
contentUploadStateTracker.setProgress(it, current, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||
|
||||
return try {
|
||||
// Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should
|
||||
// copy it to a cache folder by using InputStream and OutputStream.
|
||||
// https://github.com/zetbaitsu/Compressor/pull/150
|
||||
// As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile.
|
||||
var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir)
|
||||
cacheFile.parentFile?.mkdirs()
|
||||
if (cacheFile.exists()) {
|
||||
cacheFile.delete()
|
||||
}
|
||||
cacheFile.createNewFile()
|
||||
cacheFile.deleteOnExit()
|
||||
|
||||
val outputStream = FileOutputStream(cacheFile)
|
||||
outputStream.use {
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
|
||||
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
|
||||
cacheFile = Compressor.compress(context, cacheFile) {
|
||||
default(
|
||||
width = MAX_IMAGE_SIZE,
|
||||
height = MAX_IMAGE_SIZE
|
||||
)
|
||||
}.also { compressedFile ->
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
|
||||
val fileSize = compressedFile.length().toInt()
|
||||
newImageAttributes = NewImageAttributes(
|
||||
options.outWidth,
|
||||
options.outHeight,
|
||||
fileSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val contentUploadResponse = if (params.isEncrypted) {
|
||||
Timber.v("Encrypt file")
|
||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType())
|
||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
||||
fileUploader
|
||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||
} else {
|
||||
fileUploader
|
||||
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||
}
|
||||
|
||||
// If it's a file update the file service so that it does not redownload?
|
||||
if (params.attachment.type == ContentAttachmentData.Type.FILE) {
|
||||
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
||||
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
||||
}
|
||||
}
|
||||
|
||||
handleSuccess(params,
|
||||
contentUploadResponse.contentUri,
|
||||
uploadedFileEncryptedFileInfo,
|
||||
uploadedThumbnailUrl,
|
||||
uploadedThumbnailEncryptedFileInfo,
|
||||
newImageAttributes)
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t)
|
||||
handleFailure(params, t)
|
||||
// always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows
|
||||
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
.also { filesToDelete.add(it) }
|
||||
workingFile.outputStream().use { outputStream ->
|
||||
inputStream.use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
val uploadThumbnailResult = dealWithThumbnail(params)
|
||||
|
||||
val progressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
notifyTracker(params) {
|
||||
if (isStopped) {
|
||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||
} else {
|
||||
contentUploadStateTracker.setProgress(it, current, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||
|
||||
return try {
|
||||
val fileToUpload: File
|
||||
var newImageAttributes: NewImageAttributes? = null
|
||||
|
||||
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
|
||||
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
||||
.also { compressedFile ->
|
||||
// Get new Bitmap size
|
||||
compressedFile.inputStream().use {
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
val bitmap = BitmapFactory.decodeStream(it, null, options)
|
||||
val fileSize = bitmap?.byteCount ?: 0
|
||||
newImageAttributes = NewImageAttributes(
|
||||
options.outWidth,
|
||||
options.outHeight,
|
||||
fileSize
|
||||
)
|
||||
}
|
||||
}
|
||||
.also { filesToDelete.add(it) }
|
||||
} else {
|
||||
fileToUpload = workingFile
|
||||
}
|
||||
|
||||
val contentUploadResponse = if (params.isEncrypted) {
|
||||
Timber.v("## FileService: Encrypt file")
|
||||
|
||||
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
.also { filesToDelete.add(it) }
|
||||
|
||||
uploadedFileEncryptedFileInfo =
|
||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
|
||||
notifyTracker(params) {
|
||||
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## FileService: Uploading file")
|
||||
|
||||
fileUploader
|
||||
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
|
||||
} else {
|
||||
Timber.v("## FileService: Clear file")
|
||||
fileUploader
|
||||
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||
}
|
||||
|
||||
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
|
||||
try {
|
||||
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
||||
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
||||
}
|
||||
Timber.v("## FileService: cache storage updated")
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## FileService: Failed to update file cache")
|
||||
}
|
||||
|
||||
handleSuccess(params,
|
||||
contentUploadResponse.contentUri,
|
||||
uploadedFileEncryptedFileInfo,
|
||||
uploadThumbnailResult?.uploadedThumbnailUrl,
|
||||
uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo,
|
||||
newImageAttributes)
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
|
||||
handleFailure(params, t)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Timber.e(e, "## FileService: ERROR")
|
||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||
return Result.success(
|
||||
WorkerParamsFactory.toData(
|
||||
|
@ -241,9 +222,61 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
)
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
// Delete all temporary files
|
||||
filesToDelete.forEach {
|
||||
tryThis { it.delete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class UploadThumbnailResult(
|
||||
val uploadedThumbnailUrl: String,
|
||||
val uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo?
|
||||
)
|
||||
|
||||
/**
|
||||
* If appropriate, it will create and upload a thumbnail
|
||||
*/
|
||||
private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? {
|
||||
return ThumbnailExtractor.extractThumbnail(context, params.attachment)
|
||||
?.let { thumbnailData ->
|
||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (params.isEncrypted) {
|
||||
Timber.v("Encrypt thumbnail")
|
||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
|
||||
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
"thumb_${params.attachment.name}",
|
||||
"application/octet-stream",
|
||||
thumbnailProgressListener)
|
||||
UploadThumbnailResult(
|
||||
contentUploadResponse.contentUri,
|
||||
encryptionResult.encryptedFileInfo
|
||||
)
|
||||
} else {
|
||||
val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||
"thumb_${params.attachment.name}",
|
||||
thumbnailData.mimeType,
|
||||
thumbnailProgressListener)
|
||||
UploadThumbnailResult(
|
||||
contentUploadResponse.contentUri,
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Thumbnail upload failed")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) }
|
||||
|
||||
|
@ -262,7 +295,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
thumbnailUrl: String?,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Result {
|
||||
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
|
||||
|
||||
val updatedEvents = params.events
|
||||
|
@ -271,7 +303,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
}
|
||||
|
||||
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
|
||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
||||
return Result.success(WorkerParamsFactory.toData(sendParams)).also {
|
||||
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEvent(event: Event,
|
||||
|
|
|
@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
|
|||
// private fun URL.toKey() = toString()
|
||||
|
||||
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
|
||||
if (done) {
|
||||
updateState(url, ContentDownloadStateTracker.State.Success)
|
||||
} else {
|
||||
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
|
||||
mainHandler.post {
|
||||
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
|
||||
if (done) {
|
||||
updateState(url, ContentDownloadStateTracker.State.Success)
|
||||
} else {
|
||||
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(url: String, errorCode: Int) {
|
||||
Timber.v("## DL Progress Error code:$errorCode")
|
||||
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
|
||||
listeners[url]?.forEach {
|
||||
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
|
||||
mainHandler.post {
|
||||
Timber.v("## DL Progress Error code:$errorCode")
|
||||
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
|
||||
listeners[url]?.forEach {
|
||||
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.identity.FoundThreePid
|
|||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url
|
||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
|
@ -32,6 +31,7 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetail
|
|||
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams
|
||||
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.base64ToBase64Url
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
return rules.firstOrNull { rule ->
|
||||
// All conditions must hold true for an event in order to apply the action for the event.
|
||||
rule.enabled && rule.conditions?.all {
|
||||
it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false
|
||||
it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddEmailBody(
|
||||
/**
|
||||
* Required. A unique string generated by the client, and used to identify the validation attempt.
|
||||
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
|
||||
* 255 characters and it must not be empty.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The email address to validate.
|
||||
*/
|
||||
@Json(name = "email")
|
||||
val email: String,
|
||||
|
||||
/**
|
||||
* Required. The server will only send an email if the send_attempt is a number greater than the most
|
||||
* recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly
|
||||
* sending the same email in the case of request retries between the POSTing user and the identity server.
|
||||
* The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
|
||||
* If they do not, the server should respond with success but not resend the email.
|
||||
*/
|
||||
@Json(name = "send_attempt")
|
||||
val sendAttempt: Int
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddEmailResponse(
|
||||
/**
|
||||
* Required. The session ID. Session IDs are opaque strings that must consist entirely
|
||||
* of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddMsisdnBody(
|
||||
/**
|
||||
* Required. A unique string generated by the client, and used to identify the validation attempt.
|
||||
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
|
||||
* 255 characters and it must not be empty.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in
|
||||
* phone_number should be parsed as if it were dialled from.
|
||||
*/
|
||||
@Json(name = "country")
|
||||
val country: String,
|
||||
|
||||
/**
|
||||
* Required. The phone number to validate.
|
||||
*/
|
||||
@Json(name = "phone_number")
|
||||
val phoneNumber: String,
|
||||
|
||||
/**
|
||||
* Required. The server will only send an SMS if the send_attempt is a number greater than the most
|
||||
* recent one which it has seen, scoped to that country + phone_number + client_secret triple. This
|
||||
* is to avoid repeatedly sending the same SMS in the case of request retries between the POSTing user
|
||||
* and the identity server. The client should increment this value if they desire a new SMS (e.g. a
|
||||
* reminder) to be sent.
|
||||
*/
|
||||
@Json(name = "send_attempt")
|
||||
val sendAttempt: Int
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddMsisdnResponse(
|
||||
/**
|
||||
* Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
|
||||
* Their length must not exceed 255 characters and they must not be empty.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String,
|
||||
|
||||
/**
|
||||
* An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
|
||||
* Service API's POST /validate/email/submitToken endpoint (without the requirement for an access token).
|
||||
* The homeserver must send this token to the user (if applicable), who should then be prompted to provide it to the client.
|
||||
*
|
||||
* If this field is not present, the client can assume that verification will happen without the client's involvement provided
|
||||
* the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
|
||||
*/
|
||||
@Json(name = "submit_url")
|
||||
val submitUrl: String? = null,
|
||||
|
||||
/* ==========================================================================================
|
||||
* It seems that the homeserver is sending more data, we may need it
|
||||
* ========================================================================================== */
|
||||
|
||||
@Json(name = "msisdn")
|
||||
val msisdn: String? = null,
|
||||
|
||||
@Json(name = "intl_fmt")
|
||||
val formattedMsisdn: String? = null,
|
||||
|
||||
@Json(name = "success")
|
||||
val success: Boolean? = null
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class AddThreePidTask : Task<AddThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultAddThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val eventBus: EventBus) : AddThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
when (params.threePid) {
|
||||
is ThreePid.Email -> addEmail(params.threePid)
|
||||
is ThreePid.Msisdn -> addMsisdn(params.threePid)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addEmail(threePid: ThreePid.Email) {
|
||||
val clientSecret = UUID.randomUUID().toString()
|
||||
val sendAttempt = 1
|
||||
|
||||
val result = executeRequest<AddEmailResponse>(eventBus) {
|
||||
val body = AddEmailBody(
|
||||
clientSecret = clientSecret,
|
||||
email = threePid.email,
|
||||
sendAttempt = sendAttempt
|
||||
)
|
||||
apiCall = profileAPI.addEmail(body)
|
||||
}
|
||||
|
||||
// Store as a pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
PendingThreePid(
|
||||
threePid = threePid,
|
||||
clientSecret = clientSecret,
|
||||
sendAttempt = sendAttempt,
|
||||
sid = result.sid,
|
||||
submitUrl = null
|
||||
)
|
||||
.let { pendingThreePidMapper.map(it) }
|
||||
.let { realm.copyToRealm(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addMsisdn(threePid: ThreePid.Msisdn) {
|
||||
val clientSecret = UUID.randomUUID().toString()
|
||||
val sendAttempt = 1
|
||||
|
||||
// Get country code and national number from the phone number
|
||||
val phoneNumber = threePid.msisdn
|
||||
val phoneNumberUtil = PhoneNumberUtil.getInstance()
|
||||
val parsedNumber = phoneNumberUtil.parse(phoneNumber, null)
|
||||
val countryCode = parsedNumber.countryCode
|
||||
val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode)
|
||||
|
||||
val result = executeRequest<AddMsisdnResponse>(eventBus) {
|
||||
val body = AddMsisdnBody(
|
||||
clientSecret = clientSecret,
|
||||
country = country,
|
||||
phoneNumber = parsedNumber.nationalNumber.toString(),
|
||||
sendAttempt = sendAttempt
|
||||
)
|
||||
apiCall = profileAPI.addMsisdn(body)
|
||||
}
|
||||
|
||||
// Store as a pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
PendingThreePid(
|
||||
threePid = threePid,
|
||||
clientSecret = clientSecret,
|
||||
sendAttempt = sendAttempt,
|
||||
sid = result.sid,
|
||||
submitUrl = result.submitUrl
|
||||
)
|
||||
.let { pendingThreePidMapper.map(it) }
|
||||
.let { realm.copyToRealm(it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
|
|||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||
|
@ -44,6 +45,11 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
private val setDisplayNameTask: SetDisplayNameTask,
|
||||
private val setAvatarUrlTask: SetAvatarUrlTask,
|
||||
private val addThreePidTask: AddThreePidTask,
|
||||
private val validateSmsCodeTask: ValidateSmsCodeTask,
|
||||
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
|
||||
private val deleteThreePidTask: DeleteThreePidTask,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val fileUploader: FileUploader) : ProfileService {
|
||||
|
||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
|
@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
|
||||
if (refreshData) {
|
||||
// Force a refresh of the values
|
||||
refreshUserThreePidsTask
|
||||
.configureWith()
|
||||
.executeBy(taskExecutor)
|
||||
refreshThreePids()
|
||||
}
|
||||
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
|
@ -126,6 +130,95 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
{ it.asDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun refreshThreePids() {
|
||||
refreshUserThreePidsTask
|
||||
.configureWith()
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getPendingThreePids(): List<ThreePid> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ it.where<PendingThreePidEntity>() },
|
||||
{ pendingThreePidMapper.map(it).threePid }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ it.where<PendingThreePidEntity>() },
|
||||
{ pendingThreePidMapper.map(it).threePid }
|
||||
)
|
||||
}
|
||||
|
||||
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return addThreePidTask
|
||||
.configureWith(AddThreePidTask.Params(threePid)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return validateSmsCodeTask
|
||||
.configureWith(ValidateSmsCodeTask.Params(threePid, code)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun finalizeAddingThreePid(threePid: ThreePid,
|
||||
uiaSession: String?,
|
||||
accountPassword: String?,
|
||||
matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return finalizeAddingThreePidTask
|
||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||
threePid = threePid,
|
||||
session = uiaSession,
|
||||
accountPassword = accountPassword,
|
||||
userWantsToCancel = false
|
||||
)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return finalizeAddingThreePidTask
|
||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||
threePid = threePid,
|
||||
session = null,
|
||||
accountPassword = null,
|
||||
userWantsToCancel = true
|
||||
)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the callback to fetch 3Pids from the server in case of success
|
||||
*/
|
||||
private fun alsoRefresh(callback: MatrixCallback<Unit>): MatrixCallback<Unit> {
|
||||
return object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
refreshThreePids()
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return deleteThreePidTask
|
||||
.configureWith(DeleteThreePidTask.Params(threePid)) {
|
||||
callback = alsoRefresh(matrixCallback)
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserThreePidEntity.asDomain(): ThreePid {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteThreePidBody(
|
||||
/**
|
||||
* Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"]
|
||||
*/
|
||||
@Json(name = "medium") val medium: String,
|
||||
/**
|
||||
* Required. The third party address being removed.
|
||||
*/
|
||||
@Json(name = "address") val address: String
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class DeleteThreePidResponse(
|
||||
/**
|
||||
* Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from
|
||||
* the identity server. success indicates that the identity server has unbound the identifier
|
||||
* whereas no-support indicates that the identity server refuses to support the request or the
|
||||
* homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"]
|
||||
*/
|
||||
@Json(name = "id_server_unbind_result")
|
||||
val idServerUnbindResult: String? = null
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class DeleteThreePidTask : Task<DeleteThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultDeleteThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
private val eventBus: EventBus) : DeleteThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
executeRequest<DeleteThreePidResponse>(eventBus) {
|
||||
val body = DeleteThreePidBody(
|
||||
medium = params.threePid.toMedium(),
|
||||
address = params.threePid.value
|
||||
)
|
||||
apiCall = profileAPI.deleteThreePid(body)
|
||||
}
|
||||
|
||||
// We do not really care about the result for the moment
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class FinalizeAddThreePidBody(
|
||||
/**
|
||||
* Required. The client secret used in the session with the homeserver.
|
||||
*/
|
||||
@Json(name = "client_secret")
|
||||
val clientSecret: String,
|
||||
|
||||
/**
|
||||
* Required. The session identifier given by the homeserver.
|
||||
*/
|
||||
@Json(name = "sid")
|
||||
val sid: String,
|
||||
|
||||
/**
|
||||
* Additional authentication information for the user-interactive authentication API.
|
||||
*/
|
||||
@Json(name = "auth")
|
||||
val auth: UserPasswordAuth?
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid,
|
||||
val session: String?,
|
||||
val accountPassword: String?,
|
||||
val userWantsToCancel: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
@UserId private val userId: String,
|
||||
private val eventBus: EventBus) : FinalizeAddingThreePidTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
if (params.userWantsToCancel.not()) {
|
||||
// Get the required pending data
|
||||
val pendingThreePids = monarchy.fetchAllMappedSync(
|
||||
{ it.where(PendingThreePidEntity::class.java) },
|
||||
{ pendingThreePidMapper.map(it) }
|
||||
)
|
||||
.firstOrNull { it.threePid == params.threePid }
|
||||
?: throw IllegalArgumentException("unknown threepid")
|
||||
|
||||
try {
|
||||
executeRequest<Unit>(eventBus) {
|
||||
val body = FinalizeAddThreePidBody(
|
||||
clientSecret = pendingThreePids.clientSecret,
|
||||
sid = pendingThreePids.sid,
|
||||
auth = if (params.session != null && params.accountPassword != null) {
|
||||
UserPasswordAuth(
|
||||
session = params.session,
|
||||
user = userId,
|
||||
password = params.accountPassword
|
||||
)
|
||||
} else null
|
||||
)
|
||||
apiCall = profileAPI.finalizeAddThreePid(body)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
throw throwable.toRegistrationFlowResponse()
|
||||
?.let { Failure.RegistrationFlowError(it) }
|
||||
?: throwable
|
||||
}
|
||||
}
|
||||
|
||||
cleanupDatabase(params)
|
||||
}
|
||||
|
||||
private suspend fun cleanupDatabase(params: Params) {
|
||||
// Delete the pending three pid
|
||||
monarchy.awaitTransaction { realm ->
|
||||
realm.where(PendingThreePidEntity::class.java)
|
||||
.equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value)
|
||||
.or()
|
||||
.equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value)
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
||||
internal data class PendingThreePid(
|
||||
val threePid: ThreePid,
|
||||
val clientSecret: String,
|
||||
val sendAttempt: Int,
|
||||
// For Msisdn and Email
|
||||
val sid: String,
|
||||
// For Msisdn only
|
||||
val submitUrl: String?
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PendingThreePidMapper @Inject constructor() {
|
||||
|
||||
fun map(entity: PendingThreePidEntity): PendingThreePid {
|
||||
return PendingThreePid(
|
||||
threePid = entity.email?.let { ThreePid.Email(it) }
|
||||
?: entity.msisdn?.let { ThreePid.Msisdn(it) }
|
||||
?: error("Invalid data"),
|
||||
clientSecret = entity.clientSecret,
|
||||
sendAttempt = entity.sendAttempt,
|
||||
sid = entity.sid,
|
||||
submitUrl = entity.submitUrl
|
||||
)
|
||||
}
|
||||
|
||||
fun map(domain: PendingThreePid): PendingThreePidEntity {
|
||||
return PendingThreePidEntity(
|
||||
email = domain.threePid.takeIf { it is ThreePid.Email }?.value,
|
||||
msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value,
|
||||
clientSecret = domain.clientSecret,
|
||||
sendAttempt = domain.sendAttempt,
|
||||
sid = domain.sid,
|
||||
submitUrl = domain.submitUrl
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@
|
|||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
|
||||
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
|
@ -26,9 +28,9 @@ import retrofit2.http.GET
|
|||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Url
|
||||
|
||||
internal interface ProfileAPI {
|
||||
|
||||
/**
|
||||
* Get the combined profile information for this user.
|
||||
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
|
||||
|
@ -71,4 +73,35 @@ internal interface ProfileAPI {
|
|||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
|
||||
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken")
|
||||
fun addEmail(@Body body: AddEmailBody): Call<AddEmailResponse>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken")
|
||||
fun addMsisdn(@Body body: AddMsisdnBody): Call<AddMsisdnResponse>
|
||||
|
||||
/**
|
||||
* Validate Msisdn code (same model than for Identity server API)
|
||||
*/
|
||||
@POST
|
||||
fun validateMsisdn(@Url url: String,
|
||||
@Body params: ValidationCodeBody): Call<SuccessResult>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add")
|
||||
fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete")
|
||||
fun deleteThreePid(@Body body: DeleteThreePidBody): Call<DeleteThreePidResponse>
|
||||
}
|
||||
|
|
|
@ -58,4 +58,16 @@ internal abstract class ProfileModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindValidateSmsCodeTask(task: DefaultValidateSmsCodeTask): ValidateSmsCodeTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.profile
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
|
||||
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ValidateSmsCodeTask : Task<ValidateSmsCodeTask.Params, Unit> {
|
||||
data class Params(
|
||||
val threePid: ThreePid.Msisdn,
|
||||
val code: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultValidateSmsCodeTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
@SessionDatabase
|
||||
private val monarchy: Monarchy,
|
||||
private val pendingThreePidMapper: PendingThreePidMapper,
|
||||
private val eventBus: EventBus
|
||||
) : ValidateSmsCodeTask {
|
||||
|
||||
override suspend fun execute(params: ValidateSmsCodeTask.Params) {
|
||||
// Search the pending ThreePid
|
||||
val pendingThreePids = monarchy.fetchAllMappedSync(
|
||||
{ it.where(PendingThreePidEntity::class.java) },
|
||||
{ pendingThreePidMapper.map(it) }
|
||||
)
|
||||
.firstOrNull { it.threePid == params.threePid }
|
||||
?: throw IllegalArgumentException("unknown threepid")
|
||||
|
||||
val url = pendingThreePids.submitUrl ?: throw IllegalArgumentException("invalid threepid")
|
||||
val body = ValidationCodeBody(
|
||||
clientSecret = pendingThreePids.clientSecret,
|
||||
sid = pendingThreePids.sid,
|
||||
code = params.code
|
||||
)
|
||||
val result = executeRequest<SuccessResult>(eventBus) {
|
||||
apiCall = profileAPI.validateMsisdn(url, body)
|
||||
}
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
throw Failure.SuccessError
|
||||
}
|
||||
}
|
||||
}
|
|
@ -347,7 +347,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
|
|||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
|
||||
} else {
|
||||
Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ")
|
||||
}
|
||||
|
@ -356,7 +356,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
|
|||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
|
||||
}
|
||||
sumModel.votes = votes
|
||||
if (isLocalEcho) {
|
||||
|
|
|
@ -130,21 +130,6 @@ internal interface RoomAPI {
|
|||
@Body content: Content?
|
||||
): Call<SendResponse>
|
||||
|
||||
/**
|
||||
* Send an event to a room.
|
||||
*
|
||||
* @param txId the transaction Id
|
||||
* @param roomId the room id
|
||||
* @param eventType the event type
|
||||
* @param content the event content as string
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}")
|
||||
fun send(@Path("txId") txId: String,
|
||||
@Path("roomId") roomId: String,
|
||||
@Path("eventType") eventType: String,
|
||||
@Body content: String?
|
||||
): Call<SendResponse>
|
||||
|
||||
/**
|
||||
* Get the context surrounding an event.
|
||||
*
|
||||
|
@ -235,9 +220,9 @@ internal interface RoomAPI {
|
|||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}")
|
||||
fun sendRelation(@Path("roomId") roomId: String,
|
||||
@Path("parentId") parent_id: String,
|
||||
@Path("parent_id") parentId: String,
|
||||
@Path("relation_type") relationType: String,
|
||||
@Path("eventType") eventType: String,
|
||||
@Path("event_type") eventType: String,
|
||||
@Body content: Content?
|
||||
): Call<SendResponse>
|
||||
|
||||
|
@ -311,16 +296,16 @@ internal interface RoomAPI {
|
|||
* This cannot be undone.
|
||||
* Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there.
|
||||
*
|
||||
* @param txId the transaction Id
|
||||
* @param roomId the room id
|
||||
* @param eventId the event to delete
|
||||
* @param txId the transaction Id
|
||||
* @param roomId the room id
|
||||
* @param eventId the event to delete
|
||||
* @param reason json containing reason key {"reason": "Indecent material"}
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}")
|
||||
fun redactEvent(
|
||||
@Path("txnId") txId: String,
|
||||
@Path("roomId") roomId: String,
|
||||
@Path("eventId") parent_id: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Body reason: Map<String, String>
|
||||
): Call<SendResponse>
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ package org.matrix.android.sdk.internal.session.room
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
|
@ -75,9 +77,6 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
|
|||
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
|
||||
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
|
||||
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.commonmark.renderer.text.TextContentRenderer
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
|
@ -105,14 +104,6 @@ internal abstract class RoomModule {
|
|||
.builder()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
fun providesTextContentRenderer(): TextContentRenderer {
|
||||
return TextContentRenderer
|
||||
.builder()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
|
|
|
@ -57,8 +57,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
|
||||
invites.map {
|
||||
ThreePidInviteBody(
|
||||
id_server = identityServerUrlWithoutProtocol,
|
||||
id_access_token = identityServerAccessToken,
|
||||
idServer = identityServerUrlWithoutProtocol,
|
||||
idAccessToken = identityServerAccessToken,
|
||||
medium = it.toMedium(),
|
||||
address = it.value
|
||||
)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.membership
|
||||
|
||||
import android.content.Context
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.R
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -34,14 +34,15 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
|||
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.internal.util.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class computes room display name
|
||||
*/
|
||||
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
|
||||
@UserId private val userId: String
|
||||
internal class RoomDisplayNameResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
@UserId private val userId: String
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -89,7 +90,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
|||
.findFirst()
|
||||
?.displayName
|
||||
} else {
|
||||
context.getString(R.string.room_displayname_room_invite)
|
||||
stringProvider.getString(R.string.room_displayname_room_invite)
|
||||
}
|
||||
} else if (roomEntity?.membership == Membership.JOIN) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
|
@ -108,13 +109,13 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
|||
}
|
||||
val otherMembersCount = otherMembersSubset.count()
|
||||
name = when (otherMembersCount) {
|
||||
0 -> context.getString(R.string.room_displayname_empty_room)
|
||||
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
|
||||
2 -> context.getString(R.string.room_displayname_two_members,
|
||||
0 -> stringProvider.getString(R.string.room_displayname_empty_room)
|
||||
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
|
||||
2 -> stringProvider.getString(R.string.room_displayname_two_members,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
|
||||
)
|
||||
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
|
||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.membership.threepid
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
|
@ -28,7 +29,6 @@ import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
|
|||
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> {
|
||||
|
@ -55,8 +55,8 @@ internal class DefaultInviteThreePidTask @Inject constructor(
|
|||
|
||||
return executeRequest(eventBus) {
|
||||
val body = ThreePidInviteBody(
|
||||
id_server = identityServerUrlWithoutProtocol,
|
||||
id_access_token = identityServerAccessToken,
|
||||
idServer = identityServerUrlWithoutProtocol,
|
||||
idAccessToken = identityServerAccessToken,
|
||||
medium = params.threePid.toMedium(),
|
||||
address = params.threePid.value
|
||||
)
|
||||
|
|
|
@ -25,18 +25,22 @@ internal data class ThreePidInviteBody(
|
|||
/**
|
||||
* Required. The hostname+port of the identity server which should be used for third party identifier lookups.
|
||||
*/
|
||||
@Json(name = "id_server") val id_server: String,
|
||||
@Json(name = "id_server")
|
||||
val idServer: String,
|
||||
/**
|
||||
* Required. An access token previously registered with the identity server. Servers can treat this as optional
|
||||
* to distinguish between r0.5-compatible clients and this specification version.
|
||||
*/
|
||||
@Json(name = "id_access_token") val id_access_token: String,
|
||||
@Json(name = "id_access_token")
|
||||
val idAccessToken: String,
|
||||
/**
|
||||
* Required. The kind of address being passed in the address field, for example email.
|
||||
*/
|
||||
@Json(name = "medium") val medium: String,
|
||||
@Json(name = "medium")
|
||||
val medium: String,
|
||||
/**
|
||||
* Required. The invitee's third party identifier.
|
||||
*/
|
||||
@Json(name = "address") val address: String
|
||||
@Json(name = "address")
|
||||
val address: String
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
package org.matrix.android.sdk.internal.session.room.notification
|
||||
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
import org.matrix.android.sdk.api.pushrules.Condition
|
||||
import org.matrix.android.sdk.api.pushrules.Kind
|
||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
||||
import org.matrix.android.sdk.api.pushrules.getActions
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushCondition
|
||||
|
@ -59,7 +59,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule?
|
|||
}
|
||||
else -> {
|
||||
val condition = PushCondition(
|
||||
kind = Condition.Kind.EventMatch.value,
|
||||
kind = Kind.EventMatch.value,
|
||||
key = "room_id",
|
||||
pattern = roomId
|
||||
)
|
||||
|
|
|
@ -208,7 +208,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -31,7 +32,6 @@ import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
|||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -92,7 +92,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
|
|||
executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.sendRelation(
|
||||
roomId = roomId,
|
||||
parent_id = relatedEventId,
|
||||
parentId = relatedEventId,
|
||||
relationType = relationType,
|
||||
eventType = localEvent.type,
|
||||
content = localEvent.content
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* We cannot use work manager cancellation mechanism because cancelling a work will just ignore
|
||||
* any follow up send that was already queued.
|
||||
* We use this class to track cancel requests, the workers will look for this to check for cancellation request
|
||||
* and just ignore the work request and continue by returning success.
|
||||
*
|
||||
* Known limitation, for now requests are not persisted
|
||||
*/
|
||||
@SessionScope
|
||||
internal class CancelSendTracker @Inject constructor() {
|
||||
|
||||
data class Request(
|
||||
val localId: String,
|
||||
val roomId: String
|
||||
)
|
||||
|
||||
private val cancellingRequests = ArrayList<Request>()
|
||||
|
||||
fun markLocalEchoForCancel(eventId: String, roomId: String) {
|
||||
synchronized(cancellingRequests) {
|
||||
cancellingRequests.add(Request(eventId, roomId))
|
||||
}
|
||||
}
|
||||
|
||||
fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean {
|
||||
val index = synchronized(cancellingRequests) {
|
||||
cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
|
||||
}
|
||||
return index != -1
|
||||
}
|
||||
|
||||
fun markCancelled(eventId: String, roomId: String) {
|
||||
synchronized(cancellingRequests) {
|
||||
val index = cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
|
||||
if (index != -1) {
|
||||
cancellingRequests.removeAt(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,24 +17,35 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.Operation
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.send.SendService
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.CancelableBag
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||
|
@ -44,7 +55,6 @@ import org.matrix.android.sdk.internal.util.CancelableWork
|
|||
import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -60,7 +70,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private val cryptoService: CryptoService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val roomEventSender: RoomEventSender
|
||||
private val roomEventSender: RoomEventSender,
|
||||
private val cancelSendTracker: CancelSendTracker
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -127,48 +138,83 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
|
||||
}
|
||||
|
||||
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
|
||||
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
|
||||
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
return sendEvent(localEcho.root)
|
||||
}
|
||||
return null
|
||||
return NoOpCancellable
|
||||
}
|
||||
|
||||
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
|
||||
if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) {
|
||||
// TODO this need a refactoring of attachement sending
|
||||
// val clearContent = localEcho.root.getClearContent()
|
||||
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null
|
||||
// when (messageContent.type) {
|
||||
// MessageType.MSGTYPE_IMAGE -> {
|
||||
// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null
|
||||
// val url = imageContent.url ?: return null
|
||||
// if (url.startsWith("mxc://")) {
|
||||
// //TODO
|
||||
// } else {
|
||||
// //The image has not yet been sent
|
||||
// val attachmentData = ContentAttachmentData(
|
||||
// size = imageContent.info!!.size.toLong(),
|
||||
// mimeType = imageContent.info.mimeType!!,
|
||||
// width = imageContent.info.width.toLong(),
|
||||
// height = imageContent.info.height.toLong(),
|
||||
// name = imageContent.body,
|
||||
// path = imageContent.url,
|
||||
// type = ContentAttachmentData.Type.IMAGE
|
||||
// )
|
||||
// monarchy.runTransactionSync {
|
||||
// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let {
|
||||
// it.sendState = SendState.UNSENT
|
||||
// }
|
||||
// }
|
||||
// return internalSendMedia(localEcho.root,attachmentData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return null
|
||||
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable {
|
||||
if (localEcho.root.sendState.hasFailed()) {
|
||||
val clearContent = localEcho.root.getClearContent()
|
||||
val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent ?: return NoOpCancellable
|
||||
|
||||
val url = messageContent.getFileUrl() ?: return NoOpCancellable
|
||||
if (url.startsWith("mxc://")) {
|
||||
// We need to resend only the message as the attachment is ok
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
return sendEvent(localEcho.root)
|
||||
}
|
||||
|
||||
// we need to resend the media
|
||||
return when (messageContent) {
|
||||
is MessageImageContent -> {
|
||||
// The image has not yet been sent
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.info!!.size.toLong(),
|
||||
mimeType = messageContent.info.mimeType!!,
|
||||
width = messageContent.info.width.toLong(),
|
||||
height = messageContent.info.height.toLong(),
|
||||
name = messageContent.body,
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.IMAGE
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageVideoContent -> {
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.videoInfo?.size ?: 0L,
|
||||
mimeType = messageContent.mimeType,
|
||||
width = messageContent.videoInfo?.width?.toLong(),
|
||||
height = messageContent.videoInfo?.height?.toLong(),
|
||||
duration = messageContent.videoInfo?.duration?.toLong(),
|
||||
name = messageContent.body,
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.VIDEO
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageFileContent -> {
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.info!!.size,
|
||||
mimeType = messageContent.info.mimeType!!,
|
||||
name = messageContent.body,
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.FILE
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageAudioContent -> {
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.audioInfo?.size ?: 0,
|
||||
duration = messageContent.audioInfo?.duration?.toLong() ?: 0L,
|
||||
mimeType = messageContent.audioInfo?.mimeType,
|
||||
name = messageContent.body,
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.AUDIO
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
else -> NoOpCancellable
|
||||
}
|
||||
}
|
||||
return null
|
||||
return NoOpCancellable
|
||||
}
|
||||
|
||||
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
||||
|
@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun cancelSend(eventId: String) {
|
||||
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.deleteFailedEcho(roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resendAllFailedMessages() {
|
||||
taskExecutor.executorScope.launch {
|
||||
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
|
||||
eventsToResend.forEach {
|
||||
sendEvent(it)
|
||||
if (it.root.isTextMessage()) {
|
||||
resendTextMessage(it)
|
||||
} else if (it.root.isAttachmentMessage()) {
|
||||
resendMediaMessage(it)
|
||||
}
|
||||
}
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT)
|
||||
}
|
||||
}
|
||||
|
||||
// override fun failAllPendingMessages() {
|
||||
// taskExecutor.executorScope.launch {
|
||||
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
|
||||
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun sendMedia(attachment: ContentAttachmentData,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
|
|
@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
|
||||
@Inject lateinit var crypto: CryptoService
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.v("Start Encrypt work")
|
||||
|
@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
?: return Result.success()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
||||
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}")
|
||||
if (params.lastFailureMessage != null) {
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
|
@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
if (localEvent.eventId == null) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
|
||||
return Result.success()
|
||||
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
|
||||
}
|
||||
|
||||
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
|
||||
|
||||
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
|
||||
|
@ -120,7 +127,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
|
||||
}
|
||||
|
||||
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)
|
||||
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
} else {
|
||||
val sendState = when (error) {
|
||||
|
@ -129,8 +136,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
}
|
||||
localEchoRepository.updateSendState(localEvent.eventId, sendState)
|
||||
// always return success, or the chain will be stuck for ever!
|
||||
val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage
|
||||
?: "Error")
|
||||
val nextWorkerParams = SendEventWorker.Params(
|
||||
sessionId = params.sessionId,
|
||||
event = localEvent,
|
||||
lastFailureMessage = error?.localizedMessage ?: "Error"
|
||||
)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue