1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-07 00:32:44 +02:00

Compare commits

...

145 Commits

Author SHA1 Message Date
Benoit Marty
358fcb6b34 Merge branch 'release/0.11.0' 2019-12-19 16:44:27 +01:00
Benoit Marty
92315a4189 Prepare release 0.11.0 2019-12-19 16:44:14 +01:00
Benoit Marty
156cc1aa4a Import Strings from Riot 2019-12-19 15:50:18 +01:00
Benoit Marty
0d36e9d8a6 Merge pull request #779 from vector-im/feature/fix_some_crashes
Fix some crashes and issues
2019-12-19 14:02:19 +01:00
Benoit Marty
13439769a1 Update wording 2019-12-19 14:01:58 +01:00
Benoit Marty
bf69810f8f Bottom sheet event preview for Sticker 2019-12-19 12:05:47 +01:00
Benoit Marty
bb9510e59b Create Size data class 2019-12-19 12:05:30 +01:00
Benoit Marty
4b0dfa49f4 Limit sticker size in the timeline 2019-12-19 11:44:07 +01:00
Benoit Marty
6652965e48 Ignore lint issue 2019-12-19 10:46:11 +01:00
Benoit Marty
b0ff2cb4bb cleanup 2019-12-18 19:31:10 +01:00
Benoit Marty
648691656a Disable click on Stickers (#703) 2019-12-18 19:20:44 +01:00
Benoit Marty
7eae85a394 Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client 2019-12-18 18:41:46 +01:00
Benoit Marty
123ffe9f9c Cleanup 2019-12-18 17:00:18 +01:00
Benoit Marty
c48a439eea Add @JvmStatic for performance reasons.
See https://github.com/airbnb/MvRx/wiki/Advanced-Concepts#mvrxviewmodel
2019-12-18 16:03:10 +01:00
Benoit Marty
9d26ba3186 Fix rendering issue with HTML formatted body 2019-12-18 12:33:51 +01:00
Benoit Marty
08970ad8c1 Fix a crash on public room list
It's maybe a workaround, as it should not happen, but at least it will not crash anymore
2019-12-18 09:56:58 +01:00
Benoit Marty
79f11ad686 Prevent crash when mimetype is null 2019-12-17 17:49:28 +01:00
Benoit Marty
7fa76b9d35 Prevent crash when opening unknown room, which should not happen... 2019-12-17 16:35:04 +01:00
Benoit Marty
65faedb06b BugReport screen: improve UX when description is too short (reported by Matthew) 2019-12-17 14:26:49 +01:00
Benoit Marty
1ceddd9607 Rageshake: log resumed screens and add the log verbosity ON/OFF to the rageshakes data 2019-12-17 14:05:58 +01:00
Benoit Marty
42cdb1db11 Fix crash reported by rageshake: writeToFile may throw exceptions 2019-12-17 12:26:45 +01:00
Benoit Marty
1c727c1ee4 Fix crash reported by rageshake 2019-12-17 10:42:58 +01:00
Benoit Marty
a4aa38ee43 Fix new issue on permalink click 2019-12-16 17:14:26 +01:00
Benoit Marty
4a11d028c0 Merge pull request #706 from vector-im/feature/handle_matrix_to
Feature/handle matrix to
2019-12-16 15:50:21 +01:00
Benoit Marty
c286f2a744 ktlint 2019-12-16 15:43:58 +01:00
Benoit Marty
e2b4899b36 Internal review 2019-12-16 15:21:24 +01:00
ganfra
aa82cd2064 Update CHANGES 2019-12-16 15:16:46 +01:00
ganfra
bc568343a2 Open matrix.to with a loader 2019-12-16 15:16:46 +01:00
ganfra
abf0796794 Room alias and matrix.to link: we can now open a room though roomAlias as long as it's a joined one 2019-12-16 15:16:46 +01:00
ganfra
02febfb01b Start handling room alias 2019-12-16 15:09:41 +01:00
ganfra
91c98d4bfb Permalink: start handling permalink from outside the app 2019-12-16 15:05:55 +01:00
Benoit Marty
cfee6a43fd Merge pull request #760 from vector-im/feature/diff_match_patch_submodule
Use diff_match_patch sources as dependency
2019-12-16 15:02:29 +01:00
Benoit Marty
f14f1db0e0 Merge pull request #774 from vector-im/feature/breadcrumbs_fixes
Fix various UI issues
2019-12-16 15:00:21 +01:00
Benoit Marty
3feb2d8980 Merge pull request #768 from vector-im/feature/soft_logout
Handle invalid tokens gracefully
2019-12-16 14:57:11 +01:00
Benoit Marty
9fc3093c2c Fix issues... 2019-12-16 12:39:51 +01:00
Benoit Marty
7d910f2566 Auto review 2019-12-16 11:30:53 +01:00
Benoit Marty
0a0eda3e34 Display first letter of id if display name is empty 2019-12-16 11:08:48 +01:00
Benoit Marty
cecef5b8da Use id to get first letter, if display name is empty 2019-12-16 10:56:25 +01:00
Benoit Marty
c9ed95ed21 MatrixItem: create extension and check ids 2019-12-16 10:50:48 +01:00
Benoit Marty
3dfd6f5a69 Breadcrumbs: increase font size 2019-12-16 10:20:38 +01:00
Benoit Marty
8fc1400bab Improve user color computation and add unit tests 2019-12-14 10:38:50 +01:00
Benoit Marty
3e4b07cec3 Do not display " (IRC)") in display names 2019-12-14 10:19:11 +01:00
Benoit Marty
fbb1846694 Render default room name when it starts with an emoji (#477) 2019-12-13 21:23:18 +01:00
Benoit Marty
b435212c87 Use same default room colors than Riot-Web
And create MatrixItem
2019-12-13 20:50:32 +01:00
Benoit Marty
1108ad5705 Scroll breadcrumbs to top when opened 2019-12-13 16:50:32 +01:00
Benoit Marty
f073342954 Cleanup 2019-12-13 15:32:57 +01:00
Benoit Marty
38b40efac3 Using default values 2019-12-13 15:24:44 +01:00
Benoit Marty
e60bda7806 Better archi, better code, less bug... 2019-12-13 15:16:26 +01:00
Benoit Marty
92e60c939d ErrorFormatter: create interface 2019-12-13 14:09:27 +01:00
Benoit Marty
6e4830e325 ErrorFormatter: move it's declaration to VectorBaseFragment
and avoid duplicated code to manage default onError() in Login fragment
2019-12-13 13:58:49 +01:00
Benoit Marty
c6b98f3654 Soft Logout - display hard logout screen 2019-12-13 12:40:15 +01:00
Benoit Marty
12d54140e5 SoftLogout: also handle Unsupported mode 2019-12-13 12:08:37 +01:00
Benoit Marty
1de85daad9 SoftLogout: handle the case where user sign in with SSO on another account 2019-12-13 11:58:02 +01:00
Benoit Marty
050519e998 Soft Logout - add a TODO, waiting for Synapse bugfix 2019-12-13 09:22:24 +01:00
Benoit Marty
1af44ce5f7 cleanip 2019-12-13 01:37:29 +01:00
Benoit Marty
8d1a36425d Cleanup 2019-12-13 01:29:49 +01:00
Benoit Marty
4e74b545ad SoftLogout: recovery with SSO 2019-12-13 01:25:58 +01:00
Benoit Marty
183d6b53bd SoftLogout: start handling SSO 2019-12-13 00:20:54 +01:00
Benoit Marty
14562f7285 SoftLogout: Inherit from Login stuff to get free forgot password functionality 2019-12-13 00:08:21 +01:00
Benoit Marty
17bcd680b0 organise packages 2019-12-12 23:28:54 +01:00
Benoit Marty
954019547d Soft Logout - update comment 2019-12-12 23:25:14 +01:00
Benoit Marty
782635ec8e Keep loading after success 2019-12-12 23:20:11 +01:00
Benoit Marty
e609f4a57e SoftLogout: epoxy: missing elements 2019-12-12 23:17:03 +01:00
Benoit Marty
907fa35547 Cleanup listener 2019-12-12 22:58:27 +01:00
Benoit Marty
00d0c34363 SoftLogout: use Epoxy 2019-12-12 22:58:15 +01:00
Benoit Marty
6811d31a6d Soft Logout - request homeserver login flow 2019-12-12 20:24:46 +01:00
Benoit Marty
a464c910f8 Fix crash with Realm 2019-12-12 19:43:16 +01:00
Benoit Marty
d69881f321 cleanup 2019-12-12 17:41:16 +01:00
Benoit Marty
efc1f38f8c SoftLogout: adapt wording depending if all keys are backed up or not 2019-12-12 17:39:21 +01:00
Benoit Marty
b9e8da1fbb SoftLogout: clear notifications 2019-12-12 15:50:05 +01:00
Benoit Marty
d2fea275d8 SoftLogout: Loading UI 2019-12-12 15:33:22 +01:00
Benoit Marty
a5af949c15 SoftLogout: Store the info that the token is not valid anymore for a faster startup 2019-12-12 15:32:52 +01:00
Benoit Marty
261b4be287 Follow naming convention 2019-12-11 18:51:46 +01:00
Benoit Marty
205fc0d9d6 Soft Logout - issue with device display name 2019-12-11 18:49:44 +01:00
Benoit Marty
7699560458 Soft Logout - WIP 2019-12-11 18:35:30 +01:00
Benoit Marty
a193b2659d Create Uri extension and cleanup login code 2019-12-11 18:34:21 +01:00
Benoit Marty
bb85d41f05 Password could contain only spaces 2019-12-11 18:34:21 +01:00
Benoit Marty
9bfe904745 InvalidToken: Regular Signed out screen - move class 2019-12-11 18:34:21 +01:00
Benoit Marty
284dc8602f InvalidToken: Regular Signed out screen 2019-12-11 18:34:21 +01:00
Benoit Marty
29087d4a87 InvalidToken: Rework MainActivity args 2019-12-11 18:34:21 +01:00
Benoit Marty
18649ebddb InvalidToken: notify the app - WIP 2019-12-11 18:34:21 +01:00
Benoit Marty
d5935a13ac MatrixError: add some MatrixError from the spec and copy paste documentation 2019-12-11 18:34:21 +01:00
Benoit Marty
670d4dc34e MatrixError: rename the constants to follow the spec 2019-12-11 18:34:21 +01:00
Benoit Marty
5435a1739e SoftLogout: parse the parameter from server response 2019-12-11 18:34:21 +01:00
Benoit Marty
853518fbb2 Version++ 2019-12-11 18:34:06 +01:00
Valere
0b93f34fa0 Use diff_match_patch sources as dependency 2019-12-11 10:51:09 +01:00
Benoit Marty
902a9aa243 Merge branch 'release/0.10.0' 2019-12-10 15:47:36 +01:00
Benoit Marty
5915cebd6d Merge branch 'release/0.10.0' into develop 2019-12-10 15:47:35 +01:00
Benoit Marty
d91ff87fb9 Prepare release 0.10.0 2019-12-10 15:47:26 +01:00
Benoit Marty
79ef055bfb Merge pull request #746 from vector-im/feature/fix_various_issues
Fix 2 crashes reported by the PlayStore
2019-12-10 02:14:23 +01:00
Benoit Marty
3a761be6b4 Last cleanup 2019-12-10 01:28:07 +01:00
Benoit Marty
a9e2c31c32 Remove log for privacy 2019-12-10 01:08:05 +01:00
Benoit Marty
3ac53d20e9 Bugfix: react several times with the same reaction was possible (was a TODO). 2019-12-10 01:05:20 +01:00
Benoit Marty
3c18fd5335 Improve EmojiChooserFragment: improve filtering result: sort 2019-12-10 00:42:24 +01:00
Benoit Marty
f00f34b244 Improve EmojiChooserFragment: DI 2019-12-09 23:56:53 +01:00
Benoit Marty
63e0b15f3d Split EmojiDataSource - cleanup 2019-12-09 23:08:50 +01:00
Benoit Marty
80306f20df Split EmojiDataSource - avoid !! 2019-12-09 22:57:23 +01:00
Benoit Marty
2972177541 Split EmojiDataSource - cleanup 2019-12-09 22:46:39 +01:00
Benoit Marty
1ad8f47dc1 Split EmojiDataSource 2019-12-09 22:36:38 +01:00
Benoit Marty
8527d3f162 Improve emoji picker search result 2019-12-09 22:30:29 +01:00
Benoit Marty
99423bacb2 Cleanup 2019-12-09 22:09:17 +01:00
Benoit Marty
edc6c3dd4f Cleanup 2019-12-09 22:00:41 +01:00
Benoit Marty
a761a0dbd2 Cleanup 2019-12-09 21:56:03 +01:00
Benoit Marty
d431ab23c8 Cleanup 2019-12-09 21:33:10 +01:00
Benoit Marty
f0aa34774e Create RecyclerView extensions and cleanup all the recycler views 2019-12-09 21:31:56 +01:00
Benoit Marty
742136abe8 Create RecyclerView extensions and cleanup all the recycler views 2019-12-09 18:01:58 +01:00
Benoit Marty
36aba8554d Update CHANGES.md 2019-12-09 17:43:14 +01:00
Benoit Marty
da14ae432a Ensure we will not use EpoxyRecyclerView as a View anymore 2019-12-09 17:41:29 +01:00
ganfra
9a01b4ace9 Make it through bunch of classes removing potential leaks 2019-12-09 17:41:29 +01:00
Benoit Marty
109c1fe482 Cleanup 2019-12-09 17:41:29 +01:00
Benoit Marty
dbd4525404 Make sure unhandled Rx error does not crash the app in production 2019-12-09 17:41:29 +01:00
Benoit Marty
c714266a81 Fix crash reported by the PlayStore.
NullPointerException: at im.vector.riotx.features.home.room.detail.RoomDetailFragment.updateJumpToReadMarkerViewVisibility (RoomDetailFragment.kt:524)
Also properly cleanup model build listener
2019-12-09 17:41:29 +01:00
Benoit Marty
8b1701e537 Merge pull request #738 from vector-im/feature/ban_reason
Displaay ban and other membership events reason
2019-12-09 14:53:06 +01:00
Benoit Marty
41d1b77370 Merge pull request #749 from vector-im/feature/hs_discovery
Support entering a RiotWeb client URL instead of the homeserver URL
2019-12-09 14:10:55 +01:00
Benoit Marty
ac75fe12bf Will be merged for next release 2019-12-09 14:09:03 +01:00
Benoit Marty
2f26f4b8bb Add default value (fix test compilation issue) 2019-12-09 14:09:03 +01:00
Benoit Marty
6d82ac7c59 Add default param values 2019-12-09 14:09:03 +01:00
Benoit Marty
411afb0bf3 Add shortcut for command length 2019-12-09 14:09:03 +01:00
Benoit Marty
57354cbd69 Add reason to slash commands 2019-12-09 14:09:03 +01:00
Benoit Marty
03d51281a2 Mistake 2019-12-09 14:08:06 +01:00
Benoit Marty
415511f3e0 Shortened lines 2019-12-09 14:08:06 +01:00
Benoit Marty
e0e778909d Better formatting 2019-12-09 14:08:06 +01:00
Benoit Marty
b9efc9f4bd Ensure user will never see 'null' in a String 2019-12-09 14:08:06 +01:00
Benoit Marty
872b14373b Better code 2019-12-09 14:08:06 +01:00
Benoit Marty
d28700e2bf Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367) 2019-12-09 14:08:06 +01:00
Benoit Marty
18beef14cf "ban" event are not rendered correctly (#716) 2019-12-09 14:08:06 +01:00
Benoit Marty
e73923dca3 Merge pull request #741 from vector-im/feature/breadcrumbs
Breadcrumbs
2019-12-09 14:06:27 +01:00
Benoit Marty
94afd3e66d Add example of config without default homeserver url 2019-12-07 11:05:18 +01:00
Benoit Marty
5f540a5b45 Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744) 2019-12-06 23:46:40 +01:00
Benoit Marty
a41617e8aa Fix lint false positive issue 2019-12-06 22:00:49 +01:00
Benoit Marty
ff1745b5dc Merge pull request #742 from vector-im/feature/fix_739
Fixes #739
2019-12-06 17:10:54 +01:00
Valere
8e3e9876b8 Fixes #739 2019-12-06 10:31:40 +01:00
Benoit Marty
9a4d8f87f6 Breadcrumbs: auto-review 2019-12-05 22:38:49 +01:00
Benoit Marty
aef76241a3 Breadcrumbs: changes 2019-12-05 22:09:55 +01:00
Benoit Marty
0768bd5c88 Breadcrumbs: nicer algorithm 2019-12-05 22:05:37 +01:00
Benoit Marty
65333e6031 Cleanup some Realm queries 2019-12-05 21:49:01 +01:00
Benoit Marty
849e7c613c Breadcrumbs: live update in correct order 2019-12-05 21:27:56 +01:00
Benoit Marty
60169d53d7 Breadcrumbs: add some visual attributes 2019-12-05 20:28:07 +01:00
Benoit Marty
4234c27af9 Version++ 2019-12-05 18:19:20 +01:00
Benoit Marty
b9eb85e0a6 Merge branch 'release/0.9.1' into develop 2019-12-05 18:17:54 +01:00
Benoit Marty
5373d9aa21 Breadcrumbs: fix layout issue 2019-12-05 17:49:45 +01:00
Benoit Marty
ad4d5e5c02 Breadcrumbs: limit number to 20 2019-12-05 17:43:23 +01:00
Benoit Marty
4ff12605e9 Breadcrumbs: notify viewed rooms 2019-12-05 16:06:47 +01:00
Benoit Marty
7c561ae622 Breadcrumbs simple UI 2019-12-05 14:51:12 +01:00
Benoit Marty
cec08a20e5 Handle breadcrumbs from account data 2019-12-05 12:13:45 +01:00
336 changed files with 9423 additions and 2436 deletions

View File

@@ -18,6 +18,7 @@
<w>pbkdf</w> <w>pbkdf</w>
<w>pkcs</w> <w>pkcs</w>
<w>signin</w> <w>signin</w>
<w>signout</w>
<w>signup</w> <w>signup</w>
</words> </words>
</dictionary> </dictionary>

View File

@@ -1,3 +1,44 @@
Changes in RiotX 0.11.0 (2019-12-19)
===================================================
Features ✨:
- Implement soft logout (#281)
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
- Limit sticker size in the timeline
Other changes:
- Use same default room colors than Riot-Web
Bugfix 🐛:
- Scroll breadcrumbs to top when opened
- Render default room name when it starts with an emoji (#477)
- Do not display " (IRC)" in display names https://github.com/vector-im/riot-android/issues/444
- Fix rendering issue with HTML formatted body
- Disable click on Stickers (#703)
Build 🧱:
- Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10)
===================================================
Features ✨:
- Breadcrumbs: switch from one room to another quickly (#571)
Improvements 🙌:
- Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744)
Other changes:
- Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367)
Bugfix 🐛:
- When automardown is ON, pills are sent as MD in body (#739)
- "ban" event are not rendered correctly (#716)
- Fix crash when rotating screen in Room timeline
Changes in RiotX 0.9.1 (2019-12-05) Changes in RiotX 0.9.1 (2019-12-05)
=================================================== ===================================================

View File

@@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@@ -45,12 +45,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google() google()
jcenter() jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
} }
tasks.withType(JavaCompile).all { tasks.withType(JavaCompile).all {

1
diff-match-patch/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,8 @@
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "8"
targetCompatibility = "8"

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ Client request the sign-up flows, once the homeserver is chosen by the user and
} }
``` ```
We get the flows with a 401, which also means the the registration is possible on this homeserver. We get the flows with a 401, which also means that the registration is possible on this homeserver.
```json ```json
{ {

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.CompletableEmitter
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
override fun onSuccess(data: T) {
completableEmitter.onComplete()
}
override fun onFailure(failure: Throwable) {
completableEmitter.tryOnError(failure)
}
}
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
completableEmitter.setCancellable {
this.cancel()
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.Completable
import io.reactivex.Single
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onComplete()
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}

View File

@@ -53,12 +53,13 @@ class RxRoom(private val room: Room) {
return room.getMyReadReceiptLive().asObservable() return room.getMyReadReceiptLive().asObservable()
} }
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create { fun loadRoomMembersIfNeeded(): Single<Unit> = singleBuilder {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) room.loadRoomMembersIfNeeded(it)
} }
fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create { fun joinRoom(reason: String? = null,
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
room.join(reason, viaServers, it)
} }
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> { fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {

View File

@@ -38,6 +38,10 @@ class RxSession(private val session: Session) {
return session.liveGroupSummaries().asObservable() return session.liveGroupSummaries().asObservable()
} }
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.liveBreadcrumbs().asObservable()
}
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable() return session.syncState().asObservable()
} }
@@ -62,18 +66,25 @@ class RxSession(private val session: Session) {
return session.livePagedUsers(filter).asObservable() return session.livePagedUsers(filter).asObservable()
} }
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create { fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) session.createRoom(roomParams, it)
} }
fun searchUsersDirectory(search: String, fun searchUsersDirectory(search: String,
limit: Int, limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create { excludedUserIds: Set<String>): Single<List<User>> = singleBuilder {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) session.searchUsersDirectory(search, limit, excludedUserIds, it)
} }
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create { fun joinRoom(roomId: String,
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it) reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomId, reason, viaServers, it)
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
} }
} }

View File

@@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
sealed class LoginFlowResult { sealed class LoginFlowResult {
data class Success( data class Success(
val loginFlowResponse: LoginFlowResponse, val loginFlowResponse: LoginFlowResponse,
val isLoginAndRegistrationSupported: Boolean val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
) : LoginFlowResult() ) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult() object OutdatedHomeserver : LoginFlowResult()

View File

@@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
*/ */
data class SessionParams( data class SessionParams(
val credentials: Credentials, val credentials: Credentials,
val homeServerConnectionConfig: HomeServerConnectionConfig val homeServerConnectionConfig: HomeServerConnectionConfig,
val isTokenValid: Boolean
) )

View File

@@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError // When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody)) data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.failure
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
}

View File

@@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
/** /**
* This data class holds the error defined by the matrix specifications. * This data class holds the error defined by the matrix specifications.
* You shouldn't have to instantiate it. * You shouldn't have to instantiate it.
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
/** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String, @Json(name = "errcode") val code: String,
/** human-readable error message */
@Json(name = "error") val message: String, @Json(name = "error") val message: String,
// For M_CONSENT_NOT_GIVEN
@Json(name = "consent_uri") val consentUri: String? = null, @Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data // For M_RESOURCE_LIMIT_EXCEEDED
@Json(name = "limit_type") val limitType: String? = null, @Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null, @Json(name = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED // For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) { @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
) {
companion object { companion object {
const val FORBIDDEN = "M_FORBIDDEN" /** Forbidden access, e.g. joining a room without permission, failed login. */
const val UNKNOWN = "M_UNKNOWN" const val M_FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" /** An unknown error has occurred. */
const val MISSING_TOKEN = "M_MISSING_TOKEN" const val M_UNKNOWN = "M_UNKNOWN"
const val BAD_JSON = "M_BAD_JSON" /** The access token specified was not recognised. */
const val NOT_JSON = "M_NOT_JSON" const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val NOT_FOUND = "M_NOT_FOUND" /** No access token was specified for the request. */
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
const val USER_IN_USE = "M_USER_IN_USE" /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
const val ROOM_IN_USE = "M_ROOM_IN_USE" const val M_BAD_JSON = "M_BAD_JSON"
const val BAD_PAGINATION = "M_BAD_PAGINATION" /** Request did not contain valid JSON. */
const val UNAUTHORIZED = "M_UNAUTHORIZED" const val M_NOT_JSON = "M_NOT_JSON"
const val OLD_VERSION = "M_OLD_VERSION" /** No resource was found for this request. */
const val UNRECOGNIZED = "M_UNRECOGNIZED" const val M_NOT_FOUND = "M_NOT_FOUND"
/** Too many requests have been sent in a short period of time. Wait a while then try again. */
const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET" /* ==========================================================================================
const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" * Other error codes the client might encounter are
// Error code returned by the server when no account matches the given 3pid * ========================================================================================== */
const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
const val THREEPID_IN_USE = "M_THREEPID_IN_USE" /** Encountered when trying to register a user ID which has been taken. */
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" const val M_USER_IN_USE = "M_USER_IN_USE"
const val TOO_LARGE = "M_TOO_LARGE" /** Sent when the room alias given to the createRoom API is already in use. */
const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
/** (Not documented yet) */
const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
/** The request was not correctly authorized. Usually due to login failures. */
const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
/** (Not documented yet) */
const val M_OLD_VERSION = "M_OLD_VERSION"
/** The server did not understand the request. */
const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
/** (Not documented yet) */
const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
/** Authentication could not be performed on the third party identifier. */
const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
/** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
/** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
/** The client's request used a third party server, eg. identity server, that this server does not trust. */
const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
/** The request or entity was too large. */
const val M_TOO_LARGE = "M_TOO_LARGE"
/** (Not documented yet) */
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
* or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
* out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
* data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
/** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
/** Encountered when trying to register a user ID which is not valid. */
const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
/** Sent when the initial state given to the createRoom API is invalid. */
const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
/** The server does not permit this third party identifier. This may happen if the server only permits,
* for example, email addresses from a particular domain. */
const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
/** The client's request to create a room used a room version that the server does not support. */
const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
/** The client attempted to join a room that has a version the server does not support.
* Inspect the room_version property of the error response for the room's version. */
const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
/** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
const val M_BAD_STATE = "M_BAD_STATE"
/** The room or resource does not permit guests to access it. */
const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
/** A Captcha is required to complete the request. */
const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
/** The Captcha provided did not match what was expected. */
const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
/** A required parameter was missing from the request. */
const val M_MISSING_PARAM = "M_MISSING_PARAM"
/** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
const val M_INVALID_PARAM = "M_INVALID_PARAM"
/** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
const val M_EXCLUSIVE = "M_EXCLUSIVE"
/** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type" // Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user" const val LIMIT_TYPE_MAU = "monthly_active_user"

View File

@@ -24,9 +24,7 @@ import android.net.Uri
*/ */
sealed class PermalinkData { sealed class PermalinkData {
data class EventLink(val roomIdOrAlias: String, val eventId: String) : PermalinkData() data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
data class RoomLink(val roomIdOrAlias: String) : PermalinkData()
data class UserLink(val userId: String) : PermalinkData() data class UserLink(val userId: String) : PermalinkData()

View File

@@ -60,16 +60,21 @@ object PermalinkParser {
return PermalinkData.FallbackLink(uri) return PermalinkData.FallbackLink(uri)
} }
return when { return when {
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) { val eventId = extraParameter.takeIf {
PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter) !it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
} else {
PermalinkData.RoomLink(roomIdOrAlias = identifier)
} }
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
} }
else -> PermalinkData.FallbackLink(uri) MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
}
else -> PermalinkData.FallbackLink(uri)
} }
} }
} }

View File

@@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@@ -62,6 +62,11 @@ interface Session :
*/ */
val sessionParams: SessionParams val sessionParams: SessionParams
/**
* The session is valid, i.e. it has a valid token so far
*/
val isOpenable: Boolean
/** /**
* Useful shortcut to get access to the userId * Useful shortcut to get access to the userId
*/ */
@@ -81,7 +86,7 @@ interface Session :
/** /**
* Launches infinite periodic background syncs * Launches infinite periodic background syncs
* THis does not work in doze mode :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * If battery optimization is on it can work in app standby but that's all :/
*/ */
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
@@ -136,13 +141,10 @@ interface Session :
*/ */
interface Listener { interface Listener {
/** /**
* The access token is not valid anymore * Possible cases:
* - The access token is not valid anymore,
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/ */
fun onInvalidToken() fun onGlobalError(globalError: GlobalError)
/**
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
} }
} }

View File

@@ -30,7 +30,7 @@ data class ContentAttachmentData(
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null, val name: String? = null,
val path: String, val path: String,
val mimeType: String, val mimeType: String?,
val type: Type val type: Type
) : Parcelable { ) : Parcelable {

View File

@@ -22,6 +22,8 @@ interface ContentUploadStateTracker {
fun untrack(key: String, updateListener: UpdateListener) fun untrack(key: String, updateListener: UpdateListener)
fun clear()
interface UpdateListener { interface UpdateListener {
fun onUpdate(state: State) fun onUpdate(state: State)
} }

View File

@@ -30,12 +30,16 @@ interface RoomDirectoryService {
/** /**
* Get rooms from directory * Get rooms from directory
*/ */
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable fun getPublicRooms(server: String?,
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/** /**
* Join a room by id * Join a room by id
*/ */
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable fun joinRoom(roomId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Fetches the overall metadata about protocols supported by the homeserver. * Fetches the overall metadata about protocols supported by the homeserver.

View File

@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@@ -30,14 +31,17 @@ interface RoomService {
/** /**
* Create a room asynchronously * Create a room asynchronously
*/ */
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>): Cancelable
/** /**
* Join a room by id * Join a room by id
* @param roomId the roomId of the room to join * @param roomId the roomId of the room to join
* @param reason optional reason for joining the room
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
*/ */
fun joinRoom(roomId: String, fun joinRoom(roomId: String,
reason: String? = null,
viaServers: List<String> = emptyList(), viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable
@@ -54,8 +58,28 @@ interface RoomService {
*/ */
fun liveRoomSummaries(): LiveData<List<RoomSummary>> fun liveRoomSummaries(): LiveData<List<RoomSummary>>
/**
* Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary]
*/
fun liveBreadcrumbs(): LiveData<List<RoomSummary>>
/**
* Inform the Matrix SDK that a room is displayed.
* The SDK will update the breadcrumbs in the user account data
*/
fun onRoomDisplayed(roomId: String): Cancelable
/** /**
* Mark all rooms as read * Mark all rooms as read
*/ */
fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable fun markAllAsRead(roomIds: List<String>,
callback: MatrixCallback<Unit>): Cancelable
/**
* Resolve a room alias to a room ID.
*/
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
} }

View File

@@ -52,16 +52,20 @@ interface MembershipService {
/** /**
* Invite a user in the room * Invite a user in the room
*/ */
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable fun invite(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */
fun join(reason: String? = null,
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Leave the room, or reject an invitation. * Leave the room, or reject an invitation.
*/ */
fun leave(callback: MatrixCallback<Unit>): Cancelable fun leave(reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
} }

View File

@@ -26,9 +26,13 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomMember( data class RoomMember(
@Json(name = "membership") val membership: Membership, @Json(name = "membership") val membership: Membership,
@Json(name = "reason") val reason: String? = null,
@Json(name = "displayname") val displayName: String? = null, @Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null, @Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false, @Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null @Json(name = "unsigned") val unsignedData: UnsignedData? = null
) ) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}

View File

@@ -29,6 +29,8 @@ data class RoomSummary(
val displayName: String = "", val displayName: String = "",
val topic: String = "", val topic: String = "",
val avatarUrl: String = "", val avatarUrl: String = "",
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false, val isDirect: Boolean = false,
val latestPreviewableEvent: TimelineEvent? = null, val latestPreviewableEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(), val otherMemberIds: List<String> = emptyList(),

View File

@@ -25,7 +25,7 @@ data class VideoInfo(
/** /**
* The mimetype of the video e.g. "video/mp4". * The mimetype of the video e.g. "video/mp4".
*/ */
@Json(name = "mimetype") val mimeType: String, @Json(name = "mimetype") val mimeType: String?,
/** /**
* The width of the video in pixels. * The width of the video in pixels.

View File

@@ -50,6 +50,7 @@ interface RelationService {
/** /**
* Sends a reaction (emoji) to the targetedEvent. * Sends a reaction (emoji) to the targetedEvent.
* It has no effect if the user has already added the same reaction to the event.
* @param targetEventId the id of the event being reacted * @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji) * @param reaction the reaction (preferably emoji)
*/ */

View File

@@ -16,11 +16,12 @@
package im.vector.matrix.android.api.session.room.send package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem
/** /**
* Tag class for spans that should mention a user. * Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send * These Spans will be transformed into pills when detected in message to send
*/ */
interface UserMentionSpan { interface UserMentionSpan {
val displayName: String val matrixItem: MatrixItem
val userId: String
} }

View File

@@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
root.getClearContent().toModel<MessageStickerContent>() root.getClearContent().toModel<MessageStickerContent>()
} else { } else {
annotations?.editSummary?.aggregatedContent?.toModel() annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel() ?: root.getClearContent().toModel()
} }
} }
@@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
if (lastMessageContent != null) { if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body ?: lastMessageContent.body
} }
return null return null

View File

@@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.Cancelable
/** /**
* This interface defines a method to sign out. It's implemented at the session level. * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
*/ */
interface SignOutService { interface SignOutService {
/** /**
* Sign out * Ask the homeserver for a new access token.
* The same deviceId will be used
*/ */
fun signOut(callback: MatrixCallback<Unit>) fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable
/**
* Sign out, and release the session, clear all the session data, including crypto data
* @param sigOutFromHomeserver true if the sign out request has to be done
*/
fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable
} }

View File

@@ -17,10 +17,11 @@
package im.vector.matrix.android.api.session.sync package im.vector.matrix.android.api.session.sync
sealed class SyncState { sealed class SyncState {
object IDLE : SyncState() object Idle : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState() data class Running(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState() object Paused : SyncState()
object KILLING : SyncState() object Killing : SyncState()
object KILLED : SyncState() object Killed : SyncState()
object NO_NETWORK : SyncState() object NoNetwork : SyncState()
object InvalidToken : SyncState()
} }

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User
import java.util.*
sealed class MatrixItem(
open val id: String,
open val displayName: String?,
open val avatarUrl: String?
) {
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id
}
protected fun checkId() {
if (!id.startsWith(getIdPrefix())) {
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
}
}
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
is RoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
return getBestName()
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]
if (initial in listOf('@', '#', '+') && dn.length > 1) {
startIndex++
}
var length = 1
var first = dn[startIndex]
// LEFT-TO-RIGHT MARK
if (dn.length >= 2 && 0x200e == first.toInt()) {
startIndex++
first = dn[startIndex]
}
// check if its the start of a surrogate pair
if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
val second = dn[startIndex + 1]
if (second.toInt() in 0xDC00..0xDFFF) {
length++
}
}
dn.substring(startIndex, startIndex + length)
}
.toUpperCase(Locale.ROOT)
}
companion object {
private const val ircPattern = " (IRC)"
}
}
/* ==========================================================================================
* Extensions to create MatrixItem
* ========================================================================================== */
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)

View File

@@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.RiotConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.* import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
@@ -31,6 +32,12 @@ import retrofit2.http.*
*/ */
internal interface AuthAPI { internal interface AuthAPI {
/**
* Get a Riot config file
*/
@GET("config.json")
fun getRiotConfig(): Call<RiotConfig>
/** /**
* Get the version information of the homeserver * Get the version information of the homeserver
*/ */

View File

@@ -53,7 +53,7 @@ internal abstract class AuthModule {
.name("matrix-sdk-auth.realm") .name("matrix-sdk-auth.realm")
.modules(AuthRealmModule()) .modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION) .schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration()) .migration(AuthRealmMigration)
.build() .build()
} }
} }

View File

@@ -16,16 +16,19 @@
package im.vector.matrix.android.internal.auth package im.vector.matrix.android.internal.auth
import android.net.Uri
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.* import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.RiotConfig
import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
@@ -40,6 +43,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>, private val okHttpClient: Lazy<OkHttpClient>,
@@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
{ {
if (it is LoginFlowResult.Success) { if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config // The homeserver exists and up to date, keep the config
pendingSessionData = PendingSessionData(homeServerConnectionConfig) // Homeserver url may have been changed, if it was a Riot url
val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = Uri.parse(it.homeServerUrl)
)
pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
.also { data -> pendingSessionStore.savePendingSessionData(data) } .also { data -> pendingSessionStore.savePendingSessionData(data) }
} }
callback.onSuccess(it) callback.onSuccess(it)
@@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
.toCancelable() .toCancelable()
} }
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
return withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version
runCatching {
executeRequest<Versions> {
apiCall = authAPI.versions()
}
}
.map { versions ->
// Ok, it seems that the homeserver url is valid
getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString())
}
.fold(
{
it
},
{
if (it is Failure.OtherServerError
&& it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) {
// It's maybe a Riot url?
getRiotLoginFlowInternal(homeServerConnectionConfig)
} else {
throw it
}
}
)
}
}
private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
val authAPI = buildAuthAPI(homeServerConnectionConfig) val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version // Ok, try to get the config.json file of a RiotWeb client
val versions = executeRequest<Versions> { val riotConfig = executeRequest<RiotConfig> {
apiCall = authAPI.versions() apiCall = authAPI.getRiotConfig()
} }
if (versions.isSupportedBySdk()) { if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) {
// Ok, good sign, we got a default hs url
val newHomeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl)
)
val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig)
val versions = executeRequest<Versions> {
apiCall = newAuthAPI.versions()
}
return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl)
} else {
// Config exists, but there is no default homeserver url (ex: https://riot.im/app)
throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */)
}
}
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
return if (versions.isSupportedBySdk()) {
// Get the login flow // Get the login flow
val loginFlowResponse = executeRequest<LoginFlowResponse> { val loginFlowResponse = executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows() apiCall = authAPI.getLoginFlows()
} }
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk()) LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else { } else {
// Not supported // Not supported
LoginFlowResult.OutdatedHomeserver LoginFlowResult.OutdatedHomeserver

View File

@@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
?.also { Timber.d("Overriding identity server url to $it") } ?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) } ?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri ?: homeServerConnectionConfig.identityServerUri
)) ),
isTokenValid = true)
sessionParamsStore.save(sessionParams) sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams) return sessionManager.getOrCreateSession(sessionParams)

View File

@@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
internal interface SessionParamsStore { internal interface SessionParamsStore {
@@ -28,6 +29,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams) suspend fun save(sessionParams: SessionParams)
suspend fun setTokenInvalid(userId: String)
suspend fun updateCredentials(newCredentials: Credentials)
suspend fun delete(userId: String) suspend fun delete(userId: String)
suspend fun deleteAll() suspend fun deleteAll()

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RiotConfig(
// There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting
// Ex: "brand", "branding", etc.
@Json(name = "default_hs_url")
val defaultHomeServerUrl: String?
)

View File

@@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
internal class AuthRealmMigration : RealmMigration { internal object AuthRealmMigration : RealmMigration {
companion object { // Current schema version
// Current schema version const val SCHEMA_VERSION = 2L
const val SCHEMA_VERSION = 1L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
} }
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
} }
} }

View File

@@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth.db package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.awaitTransaction
@@ -75,6 +76,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
} }
} }
override suspend fun setTokenInvalid(userId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user $userId"
.let { Timber.w(it) }
.also { error(it) }
} else {
currentSessionParams.isTokenValid = false
}
}
}
override suspend fun updateCredentials(newCredentials: Credentials) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user ${newCredentials.userId}"
.let { Timber.w(it) }
.also { error(it) }
} else {
val newSessionParams = currentSessionParams.copy(
credentials = newCredentials,
isTokenValid = true
)
val entity = mapper.map(newSessionParams)
if (entity != null) {
realm.insertOrUpdate(entity)
}
}
}
}
override suspend fun delete(userId: String) { override suspend fun delete(userId: String) {
awaitTransaction(realmConfiguration) { awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java) it.where(SessionParamsEntity::class.java)

View File

@@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity( internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "", @PrimaryKey var userId: String = "",
var credentialsJson: String = "", var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "" var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
) : RealmObject() ) : RealmObject()

View File

@@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) { if (credentials == null || homeServerConnectionConfig == null) {
return null return null
} }
return SessionParams(credentials, homeServerConnectionConfig) return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
} }
fun map(sessionParams: SessionParams?): SessionParamsEntity? { fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentialsJson == null || homeServerConnectionConfigJson == null) { if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null return null
} }
return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson) return SessionParamsEntity(
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)
} }
} }

View File

@@ -49,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type * @param mimetype the mime type
* @return the encryption file info * @return the encryption file info
*/ */
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult { fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom() val secureRandom = SecureRandom()

View File

@@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.") Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) { when (failure.error.code) {
MatrixError.NOT_FOUND, MatrixError.M_NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> { MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version // Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure) backupAllGroupSessionsCallback?.onFailure(failure)

View File

@@ -19,16 +19,14 @@ package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
* Get or create a room * Get or create a room
*/ */
internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity {
return getById(realm, roomId) return getById(realm, roomId) ?: realm.createObject(roomId)
?: let {
realm.createObject(CryptoRoomEntity::class.java, roomId)
}
} }
/** /**

View File

@@ -20,18 +20,20 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
* Get or create a device info * Get or create a device info
*/ */
internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity {
val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId)
return realm.where<DeviceInfoEntity>() return realm.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key)
.findFirst() .findFirst()
?: let { ?: realm.createObject<DeviceInfoEntity>(key)
realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply { .apply {
this.deviceId = deviceId this.deviceId = deviceId
} }
}
} }

View File

@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
@@ -28,9 +29,7 @@ internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): Use
return realm.where<UserEntity>() return realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
.findFirst() .findFirst()
?: let { ?: realm.createObject(userId)
realm.createObject(UserEntity::class.java, userId)
}
} }
/** /**

View File

@@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor(
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState, versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList()
) )
} }
} }

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
internal open class BreadcrumbsEntity(
var recentRoomIds: RealmList<String> = RealmList()
) : RealmObject() {
companion object
}

View File

@@ -38,7 +38,11 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var readMarkerId: String? = null, var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false, var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
var flatAliases: String = ""
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
@@ -59,5 +63,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
versioningStateStr = value.name versioningStateStr = value.name
} }
companion object companion object {
const val NOT_IN_BREADCRUMBS = -1
}
} }

View File

@@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
SyncEntity::class, SyncEntity::class,
UserEntity::class, UserEntity::class,
IgnoredUserEntity::class, IgnoredUserEntity::class,
BreadcrumbsEntity::class,
EventAnnotationsSummaryEntity::class, EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class, ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class,

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? {
return realm.where<BreadcrumbsEntity>().findFirst()
}
internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity {
return get(realm) ?: realm.createObject()
}

View File

@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> { internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
@@ -28,6 +29,5 @@ internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): Rea
} }
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
return where(realm, roomId).findFirst() return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
} }

View File

@@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
@@ -44,10 +45,11 @@ internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst() return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply { ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
this.roomId = roomId .apply {
this.userId = userId this.roomId = roomId
} this.userId = userId
}
} }
private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"

View File

@@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
@@ -31,9 +32,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
return query return query
} }
internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? {
val roomSummary = realm.where<RoomSummaryEntity>()
.equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias)
.findFirst()
if (roomSummary != null) {
return roomSummary
}
return realm.where<RoomSummaryEntity>()
.contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias")
.findFirst()
}
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst() return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
} }
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {

View File

@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
@@ -34,6 +35,7 @@ object MoshiProvider {
.registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)
.registerSubtype(UserAccountDataBreadcrumbs::class.java, UserAccountData.TYPE_BREADCRUMBS)
) )
.add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)
.registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)

View File

@@ -16,19 +16,29 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import javax.inject.Inject import javax.inject.Inject
internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor { internal class AccessTokenInterceptor @Inject constructor(
@UserId private val userId: String,
private val sessionParamsStore: SessionParamsStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request() var request = chain.request()
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set accessToken?.let {
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken) val newRequestBuilder = request.newBuilder()
request = newRequestBuilder.build() // Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
request = newRequestBuilder.build()
}
return chain.proceed(request) return chain.proceed(request)
} }
private val accessToken
get() = sessionParamsStore.get(userId)?.credentials?.accessToken
} }

View File

@@ -19,8 +19,9 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import im.vector.matrix.android.api.failure.ConsentNotGivenError import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@@ -31,6 +32,7 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -98,7 +100,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) { if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management // Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
} }
return Failure.ServerError(matrixError, httpCode) return Failure.ServerError(matrixError, httpCode)
@@ -106,6 +112,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
} catch (ex: JsonDataException) { } catch (ex: JsonDataException) {
// This is not a MatrixError // This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError") Timber.w("The error returned by the server is not a MatrixError")
} catch (ex: JsonEncodingException) {
// This is not a MatrixError, HTML code?
Timber.w("The error returned by the server is not a MatrixError, probably HTML string")
} }
return Failure.OtherServerError(errorBodyStr, httpCode) return Failure.OtherServerError(errorBodyStr, httpCode)

View File

@@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## decrypt file") Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
} else { ?: throw IllegalStateException("Decryption error")
inputStream
} }
writeToFile(inputStream, destFile)
destFile
} }
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else { } else {
Try.just(destFile) Try.just(destFile)
} }

View File

@@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>, private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>) private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
@@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null private var syncThread: SyncThread? = null
override val isOpenable: Boolean
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
@MainThread @MainThread
override fun open() { override fun open() {
assertMainThread() assertMainThread()
@@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { fun onGlobalError(globalError: GlobalError) {
sessionListeners.dispatchConsentNotGiven(consentNotGivenError) if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)
}
}
sessionListeners.dispatchGlobalError(globalError)
} }
override fun contentUrlResolver() = contentUrlResolver override fun contentUrlResolver() = contentUrlResolver

View File

@@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import javax.inject.Inject import javax.inject.Inject
@@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
} }
} }
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) { fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) { synchronized(listeners) {
listeners.forEach { listeners.forEach {
it.onConsentNotGivenError(consentNotGivenError) it.onGlobalError(globalError)
} }
} }
} }

View File

@@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
} }
} }
override fun clear() {
listeners.clear()
}
internal fun setFailure(key: String, throwable: Throwable) { internal fun setFailure(key: String, throwable: Throwable) {
val failure = ContentUploadStateTracker.State.Failure(throwable) val failure = ContentUploadStateTracker.State.Failure(throwable)
updateState(key, failure) updateState(key, failure)

View File

@@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
suspend fun uploadFile(file: File, suspend fun uploadFile(file: File,
filename: String?, filename: String?,
mimeType: String, mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull()) val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }

View File

@@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable { override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId)) { .configureWith(JoinRoomTask.Params(roomId, reason)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View File

@@ -25,14 +25,17 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm import io.realm.Realm
@@ -43,6 +46,8 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {
@@ -75,9 +80,28 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
) )
} }
override fun joinRoom(roomId: String, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
},
{ roomSummaryMapper.map(it) }
)
}
override fun onRoomDisplayed(roomId: String): Cancelable {
return updateBreadcrumbsTask
.configureWith(UpdateBreadcrumbsTask.Params(roomId))
.executeBy(taskExecutor)
}
override fun joinRoom(roomId: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId, viaServers)) { .configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
@@ -90,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable {
return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View File

@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -217,7 +218,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
fun join(@Path("roomId") roomId: String, fun join(@Path("roomId") roomId: String,
@Query("server_name") viaServers: List<String>, @Query("server_name") viaServers: List<String>,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String?>): Call<Unit>
/** /**
* Leave the given room. * Leave the given room.
@@ -227,7 +228,7 @@ internal interface RoomAPI {
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
fun leave(@Path("roomId") roomId: String, fun leave(@Path("roomId") roomId: String,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String?>): Call<Unit>
/** /**
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.
@@ -258,4 +259,12 @@ internal interface RoomAPI {
fun reportContent(@Path("roomId") roomId: String, fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Body body: ReportContentBody): Call<Unit> @Body body: ReportContentBody): Call<Unit>
/**
* Get the room ID associated to the room alias.
*
* @param roomAlias the room alias.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
} }

View File

@@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.DefaultFileService
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask
@@ -133,4 +135,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
@Binds
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
} }

View File

@@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@@ -68,7 +70,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
unreadNotifications: RoomSyncUnreadNotifications? = null, unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) { updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
if (roomSummary != null) { if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
@@ -91,15 +93,24 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId) val otherRoomMembers = RoomMembers(realm, roomId)

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.alias
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
data class Params(
val roomAlias: String,
val searchOnServer: Boolean
)
}
internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monarchy: Monarchy,
private val roomAPI: RoomAPI) : GetRoomIdByAliasTask {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
}
return if (roomId != null) {
Optional.from(roomId)
} else if (!params.searchOnServer) {
Optional.from<String>(null)
} else {
roomId = executeRequest<RoomAliasDescription> {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}.roomId
Optional.from(roomId)
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.alias
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RoomAliasDescription(
/**
* The room ID for this alias.
*/
@Json(name = "room_id") val roomId: String,
/**
* A list of servers that are aware of this room ID.
*/
@Json(name = "servers") val servers: List<String> = emptyList()
)

View File

@@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params) apiCall = roomAPI.createRoom(params)
} }
val roomId = createRoomResponse.roomId!! val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm -> val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java) realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomEntityFields.ROOM_ID, roomId)

View File

@@ -83,8 +83,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
return result return result
} }
override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable { override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteTask.Params(roomId, userId) val params = InviteTask.Params(roomId, userId, reason)
return inviteTask return inviteTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback
@@ -92,8 +92,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId, viaServers) val params = JoinRoomTask.Params(roomId, reason, viaServers)
return joinTask return joinTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback
@@ -101,8 +101,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun leave(callback: MatrixCallback<Unit>): Cancelable { override fun leave(reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = LeaveRoomTask.Params(roomId) val params = LeaveRoomTask.Params(roomId, reason)
return leaveRoomTask return leaveRoomTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback

View File

@@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class InviteBody( data class InviteBody(
@Json(name = "user_id") val userId: String @Json(name = "user_id") val userId: String,
@Json(name = "reason") val reason: String?
) )

View File

@@ -24,7 +24,8 @@ import javax.inject.Inject
internal interface InviteTask : Task<InviteTask.Params, Unit> { internal interface InviteTask : Task<InviteTask.Params, Unit> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val userId: String val userId: String,
val reason: String?
) )
} }
@@ -32,7 +33,7 @@ internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAP
override suspend fun execute(params: InviteTask.Params) { override suspend fun execute(params: InviteTask.Params) {
return executeRequest { return executeRequest {
val body = InviteBody(params.userId) val body = InviteBody(params.userId, params.reason)
apiCall = roomAPI.invite(params.roomId, body) apiCall = roomAPI.invite(params.roomId, body)
} }
} }

View File

@@ -32,6 +32,7 @@ import javax.inject.Inject
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> { internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val reason: String?,
val viaServers: List<String> = emptyList() val viaServers: List<String> = emptyList()
) )
} }
@@ -43,7 +44,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
override suspend fun execute(params: JoinRoomTask.Params) { override suspend fun execute(params: JoinRoomTask.Params) {
executeRequest<Unit> { executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap()) apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason))
} }
val roomId = params.roomId val roomId = params.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)

View File

@@ -23,7 +23,8 @@ import javax.inject.Inject
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> { internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
data class Params( data class Params(
val roomId: String val roomId: String,
val reason: String?
) )
} }
@@ -31,7 +32,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: Roo
override suspend fun execute(params: LeaveRoomTask.Params) { override suspend fun execute(params: LeaveRoomTask.Params) {
return executeRequest { return executeRequest {
apiCall = roomAPI.leave(params.roomId, HashMap()) apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason))
} }
} }
} }

View File

@@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
@@ -44,6 +47,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.fetchCopyMap
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
@@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: RelationService { : RelationService {
@@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
} }
override fun sendReaction(targetEventId: String, reaction: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) return if (monarchy
.also { saveLocalEcho(it) } .fetchCopyMap(
val sendRelationWork = createSendEventWork(event, true) { realm ->
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
return CancelableWork(context, sendRelationWork.id) },
{ entity, _ ->
timelineEventMapper.map(entity)
})
?.annotations
?.reactionsSummary
.orEmpty()
.none { it.addedByMe && it.key == reaction }) {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) }
val sendRelationWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
CancelableWork(context, sendRelationWork.id)
} else {
Timber.w("Reaction already added")
NoOpCancellable
}
} }
override fun undoReaction(targetEventId: String, reaction: String): Cancelable { override fun undoReaction(targetEventId: String, reaction: String): Cancelable {

View File

@@ -78,7 +78,7 @@ internal class LocalEchoEventFactory @Inject constructor(
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (isFormattedTextPertinent(source, htmlText)) { if (isFormattedTextPertinent(source, htmlText)) {
return TextContent(source, htmlText) return TextContent(text.toString(), htmlText)
} }
} else { } else {
// Try to detect pills // Try to detect pills
@@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_AUDIO, type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
audioInfo = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg", mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_FILE, type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file", body = attachment.name ?: "file",
info = FileInfo( info = FileInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
?: "application/octet-stream", ?: "application/octet-stream",
size = attachment.size size = attachment.size
), ),

View File

@@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean { private fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {

View File

@@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // append the pill
append(String.format(template, urlSpan.userId, urlSpan.displayName)) append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
currIndex = end currIndex = end
} }
// append text after the last pill // append text after the last pill

View File

@@ -289,6 +289,9 @@ internal class DefaultTimeline(
} }
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also { listeners.add(listener).also {
postSnapshot() postSnapshot()
} }
@@ -494,9 +497,9 @@ internal class DefaultTimeline(
return return
} }
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = token,
direction = direction.toPaginationDirection(), direction = direction.toPaginationDirection(),
limit = limit) limit = limit)
Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask cancelableBag += paginationTask
@@ -571,7 +574,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
} }

View File

@@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService { private val taskExecutor: TaskExecutor) : SignOutService {
override fun signOut(callback: MatrixCallback<Unit>) { override fun signInAgain(password: String,
signOutTask callback: MatrixCallback<Unit>): Cancelable {
.configureWith { return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask
.configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> {
data class Params(
val password: String
)
}
internal class DefaultSignInAgainTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val sessionParams: SessionParams,
private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
override suspend fun execute(params: SignInAgainTask.Params) {
val newCredentials = executeRequest<Credentials> {
apiCall = signOutAPI.loginAgain(
PasswordLoginParams.userIdentifier(
// Reuse the same userId
sessionParams.credentials.userId,
params.password,
// The spec says the initial device name will be ignored
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
// but https://github.com/matrix-org/synapse/issues/6525
// Reuse the same deviceId
deviceId = sessionParams.credentials.deviceId
)
)
}
sessionParamsStore.updateCredentials(newCredentials)
}
}

View File

@@ -16,12 +16,27 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
internal interface SignOutAPI { internal interface SignOutAPI {
/**
* Attempt to login again to the same account.
* Set all the timeouts to 1 minute
* It is similar to [AuthAPI.login]
*
* @param loginParams the login parameters
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials>
/** /**
* Invalidate the access token, so that it can no longer be used for authorization. * Invalidate the access token, so that it can no longer be used for authorization.
*/ */

View File

@@ -37,8 +37,11 @@ internal abstract class SignOutModule {
} }
@Binds @Binds
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
@Binds @Binds
abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
@Binds
abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
} }

View File

@@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
import android.content.Context import android.content.Context
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.crypto.CryptoModule
@@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit> internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params(
val sigOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(private val context: Context, internal class DefaultSignOutTask @Inject constructor(private val context: Context,
@UserId private val userId: String, @UserId private val userId: String,
@@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask { @UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) { override suspend fun execute(params: SignOutTask.Params) {
Timber.d("SignOut: send request...") // It should be done even after a soft logout, to be sure the deviceId is deleted on the
executeRequest<Unit> { if (params.sigOutFromHomeserver) {
apiCall = signOutAPI.signOut() Timber.d("SignOut: send request...")
try {
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
} catch (throwable: Throwable) {
// Maybe due to https://github.com/matrix-org/synapse/issues/5755
if (throwable is Failure.ServerError
&& throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also throwable.error.isSoftLogout should be true
// Ignore
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
} else {
throw throwable
}
}
} }
Timber.d("SignOut: release session...") Timber.d("SignOut: release session...")

View File

@@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
} }
val syncResponse = try { val syncResponse = executeRequest<SyncResponse> {
executeRequest<SyncResponse> { apiCall = syncAPI.sync(requestParams)
apiCall = syncAPI.sync(requestParams)
}
} catch (throwable: Throwable) {
// Intercept 401
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete(userId)
}
throw throwable
} }
syncResponseHandler.handleResponse(syncResponse, token) syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch) syncTokenStore.saveToken(syncResponse.nextBatch)

View File

@@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.accountdata.* import im.vector.matrix.android.internal.session.sync.model.accountdata.*
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask
import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@@ -44,6 +45,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val savePushRulesTask: SavePushRulesTask, private val savePushRulesTask: SavePushRulesTask,
private val saveIgnoredUsersTask: SaveIgnoredUsersTask, private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
private val taskExecutor: TaskExecutor) { private val taskExecutor: TaskExecutor) {
suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) { suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
@@ -52,6 +54,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
is UserAccountDataDirectMessages -> handleDirectChatRooms(it) is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
is UserAccountDataPushRules -> handlePushRules(it) is UserAccountDataPushRules -> handlePushRules(it)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it) is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it)
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
else -> error("Missing code here!") else -> error("Missing code here!")
} }
@@ -130,4 +133,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
.executeBy(taskExecutor) .executeBy(taskExecutor)
// TODO If not initial sync, we should execute a init sync // TODO If not initial sync, we should execute a init sync
} }
private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
saveBreadcrumbsTask
.configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds))
.executeBy(taskExecutor)
}
} }

View File

@@ -147,7 +147,7 @@ open class SyncService : Service() {
} }
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token, stop the thread
stopSelf() stopSelf()
} }

View File

@@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { ) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.IDLE private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>() private var liveState = MutableLiveData<SyncState>()
private val lock = Object() private val lock = Object()
private var cancelableTask: Cancelable? = null private var cancelableTask: Cancelable? = null
private var isStarted = false private var isStarted = false
private var isTokenValid = true
init { init {
updateStateTo(SyncState.IDLE) updateStateTo(SyncState.Idle)
} }
fun setInitialForeground(initialForeground: Boolean) { fun setInitialForeground(initialForeground: Boolean) {
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
updateStateTo(newState) updateStateTo(newState)
} }
@@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (!isStarted) { if (!isStarted) {
Timber.v("Resume sync...") Timber.v("Resume sync...")
isStarted = true isStarted = true
// Check again the token validity
isTokenValid = true
lock.notify() lock.notify()
} }
} }
@@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
fun kill() = synchronized(lock) { fun kill() = synchronized(lock) {
Timber.v("Kill sync...") Timber.v("Kill sync...")
updateStateTo(SyncState.KILLING) updateStateTo(SyncState.Killing)
cancelableTask?.cancel() cancelableTask?.cancel()
lock.notify() lock.notify()
} }
@@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.register(this) networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this) backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) { while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state") Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) { if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...") Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK) updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else if (!isStarted) { } else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...") Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED) updateStateTo(SyncState.Paused)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isTokenValid) {
Timber.v("Token is invalid. Waiting...")
updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else { } else {
if (state !is SyncState.RUNNING) { if (state !is SyncState.Running) {
updateStateTo(SyncState.RUNNING(afterPause = true)) updateStateTo(SyncState.Running(afterPause = true))
} }
// No timeout after a pause // No timeout after a pause
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout") Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} else if (failure is Failure.Cancelled) { } else if (failure is Failure.Cancelled) {
Timber.v("Cancelled") Timber.v("Cancelled")
} else if (failure is Failure.ServerError } else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token
Timber.w(failure) Timber.w(failure)
updateStateTo(SyncState.KILLING) isTokenValid = false
isStarted = false
} else { } else {
Timber.e(failure) Timber.e(failure)
@@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await() latch.await()
state.let { state.let {
if (it is SyncState.RUNNING && it.afterPause) { if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.RUNNING(afterPause = false)) updateStateTo(SyncState.Running(afterPause = false))
} }
} }
@@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} }
} }
Timber.v("Sync killed") Timber.v("Sync killed")
updateStateTo(SyncState.KILLED) updateStateTo(SyncState.Killed)
backgroundDetectionObserver.unregister(this) backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this) networkConnectivityChecker.unregister(this)
} }

View File

@@ -25,6 +25,7 @@ internal abstract class UserAccountData {
companion object { companion object {
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
const val TYPE_DIRECT_MESSAGES = "m.direct" const val TYPE_DIRECT_MESSAGES = "m.direct"
const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets" const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules" const val TYPE_PUSH_RULES = "m.push_rules"

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataBreadcrumbs(
@Json(name = "type") override val type: String = TYPE_BREADCRUMBS,
@Json(name = "content") val content: BreadcrumbsContent
) : UserAccountData()
@JsonClass(generateAdapter = true)
internal data class BreadcrumbsContent(
@Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList()
)

View File

@@ -35,5 +35,11 @@ internal abstract class AccountDataModule {
} }
@Binds @Binds
abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
@Binds
abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
@Binds
abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
} }

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.user.accountdata
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.RealmList
import javax.inject.Inject
/**
* Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally
*/
internal interface SaveBreadcrumbsTask : Task<SaveBreadcrumbsTask.Params, Unit> {
data class Params(
val recentRoomIds: List<String>
)
}
internal class DefaultSaveBreadcrumbsTask @Inject constructor(
private val monarchy: Monarchy
) : SaveBreadcrumbsTask {
override suspend fun execute(params: SaveBreadcrumbsTask.Params) {
monarchy.awaitTransaction { realm ->
// Get or create a breadcrumbs entity
val entity = BreadcrumbsEntity.getOrCreate(realm)
// And save the new received list
entity.recentRoomIds = RealmList<String>().apply { addAll(params.recentRoomIds) }
// Update the room summaries
// Reset all the indexes...
RoomSummaryEntity.where(realm)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.findAll()
.forEach {
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
}
// ...and apply new indexes
params.recentRoomIds.forEachIndexed { index, roomId ->
RoomSummaryEntity.where(realm, roomId)
.findFirst()
?.breadcrumbsIndex = index
}
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.user.accountdata
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.query.get
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject
// Use the same arbitrary value than Riot-Web
private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20
internal interface UpdateBreadcrumbsTask : Task<UpdateBreadcrumbsTask.Params, Unit> {
data class Params(
val newTopRoomId: String
)
}
internal class DefaultUpdateBreadcrumbsTask @Inject constructor(
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val monarchy: Monarchy
) : UpdateBreadcrumbsTask {
override suspend fun execute(params: UpdateBreadcrumbsTask.Params) {
val newBreadcrumbs =
// Get the breadcrumbs entity, if any
monarchy.fetchCopied { BreadcrumbsEntity.get(it) }
?.recentRoomIds
?.apply {
// Modify the list to add the newTopRoomId first
// Ensure the newTopRoomId is not already in the list
remove(params.newTopRoomId)
// Add the newTopRoomId at first position
add(0, params.newTopRoomId)
}
?.take(MAX_BREADCRUMBS_ROOMS_NUMBER)
?: listOf(params.newTopRoomId)
// Update the DB locally, do not wait for the sync
saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs))
// FIXME It can remove the previous breadcrumbs, if not synced yet
// And update account data
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams(
breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs)
))
}
}

View File

@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
@@ -38,6 +39,15 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
return directMessages return directMessages
} }
} }
data class BreadcrumbsParams(override val type: String = UserAccountData.TYPE_BREADCRUMBS,
private val breadcrumbsContent: BreadcrumbsContent
) : Params {
override fun getData(): Any {
return breadcrumbsContent
}
}
} }
internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI, internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,

View File

@@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.util package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber import timber.log.Timber
import java.util.Locale
/** /**
* Convert a string to an UTF8 String * Convert a string to an UTF8 String
@@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
s s
} }
} }
fun String?.firstLetterOfDisplayName(): String {
if (this.isNullOrEmpty()) return ""
val isUserId = MatrixPatterns.isUserId(this)
val firstLetterIndex = if (isUserId) 1 else 0
return this[firstLetterIndex].toString().toUpperCase(Locale.ROOT)
}

View File

@@ -82,4 +82,86 @@
<string name="room_displayname_empty_room">Prázdná místnost</string> <string name="room_displayname_empty_room">Prázdná místnost</string>
<string name="notice_room_update">%s upravil/a tuto místnost.</string>
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Zpráva smazána [smazal/a %1$s] [důvod: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">"%1$s obnovil/a pozvánku do místnosti pro %2$s"</string>
<string name="verification_emoji_cat">Kočka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kůň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prase</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Králík</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohout</string>
<string name="verification_emoji_penguin">Tučnák</string>
<string name="verification_emoji_turtle">Želva</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnice</string>
<string name="verification_emoji_butterfly">Motýl</string>
<string name="verification_emoji_flower">Květina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Houba</string>
<string name="verification_emoji_globe">Glóbus</string>
<string name="verification_emoji_moon">Měsíc</string>
<string name="verification_emoji_cloud">Mrak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukuřice</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Dort</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Smajlík</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobouk</string>
<string name="verification_emoji_glasses">Brýle</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
<string name="verification_emoji_umbrella">Deštník</string>
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Dárek</string>
<string name="verification_emoji_lightbulb">Žárovka</string>
<string name="verification_emoji_book">Knížka</string>
<string name="verification_emoji_pencil">Tužka</string>
<string name="verification_emoji_paperclip">Sponka</string>
<string name="verification_emoji_scissors">Nůžky</string>
<string name="verification_emoji_lock">Zámek</string>
<string name="verification_emoji_key">Klíč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Kolo</string>
<string name="verification_emoji_airplane">Letadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Pohár</string>
<string name="verification_emoji_ball">Míč</string>
<string name="verification_emoji_guitar">Kytara</string>
<string name="verification_emoji_trumpet">Trumpeta</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Sluchátka</string>
<string name="verification_emoji_folder">Složka</string>
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
\nStahuji účet…</string>
<string name="initial_sync_start_importing_account_crypto">Uvodní synchronizace:
\nStahuji klíče</string>
<string name="initial_sync_start_importing_account_rooms">Uvodní synchnizace:
\nStahuji místnost</string>
<string name="initial_sync_start_importing_account_joined_rooms">Uvodní synchronizace:
\nStahuji moje místnosti</string>
<string name="initial_sync_start_importing_account_left_rooms">Uvodní synchonizace:
\nStahuji místnosti, které jsem opustil/a</string>
<string name="initial_sync_start_importing_account_groups">Úvodní sychronizace:
\nImportuji komunity</string>
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
\nImportuji data účtu</string>
<string name="event_status_sending_message">Posílám zprávu…</string>
</resources> </resources>

View File

@@ -49,7 +49,7 @@
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string> <string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string> <string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string> <string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">관련 대화</string> <string name="message_reply_to_prefix">관련 대화</string>

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