Merge branch 'develop' into feature/ons/fix_hide_state_events
This commit is contained in:
commit
b321838502
2
.idea/dictionaries/bmarty.xml
generated
2
.idea/dictionaries/bmarty.xml
generated
@ -24,6 +24,8 @@
|
||||
<w>pbkdf</w>
|
||||
<w>pids</w>
|
||||
<w>pkcs</w>
|
||||
<w>previewable</w>
|
||||
<w>previewables</w>
|
||||
<w>riotx</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
|
17
CHANGES.md
17
CHANGES.md
@ -4,30 +4,43 @@ Changes in Element 1.0.12 (2020-XX-XX)
|
||||
Features ✨:
|
||||
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
||||
- Room setting: update join rules and guest access (#2442)
|
||||
- Url preview (#481)
|
||||
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
|
||||
- Emoji Keyboard (#2520)
|
||||
|
||||
Improvements 🙌:
|
||||
- Add Setting Item to Change PIN (#2462)
|
||||
- Improve room history visibility setting UX (#1579)
|
||||
- Matrix.to deeplink custom scheme support
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix cancellation of sending event (#2438)
|
||||
- Double bottomsheet effect after verify with passphrase
|
||||
- EditText cursor jumps to the start while typing fast (#2469)
|
||||
- UTD for events before invitation if member state events are hidden (#2486)
|
||||
- No known servers error is given when joining rooms on new Gitter bridge (#2516)
|
||||
- Show preview when sending attachment from the keyboard (#2440)
|
||||
- Do not compress GIFs (#1616, #1254)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
- StateService now exposes suspendable function instead of using MatrixCallback.
|
||||
- RawCacheStrategy has been moved and renamed to CacheStrategy
|
||||
- FileService: remove useless FileService.DownloadMode
|
||||
|
||||
Build 🧱:
|
||||
-
|
||||
- Upgrade some dependencies and Kotlin version
|
||||
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
|
||||
- Upgrade Realm dependency to 10.1.2
|
||||
|
||||
Test:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- Remove "Status.im" theme #2424
|
||||
- Log HTTP requests and responses in production (level BASIC, i.e. without any private data)
|
||||
|
||||
Changes in Element 1.0.11 (2020-11-27)
|
||||
===================================================
|
||||
|
@ -66,7 +66,6 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
buildscript {
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
ext.kotlin_version = '1.4.10'
|
||||
ext.kotlin_coroutines_version = "1.3.9"
|
||||
ext.kotlin_version = '1.4.20'
|
||||
ext.kotlin_coroutines_version = "1.4.1"
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
@ -12,7 +12,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath 'com.google.gms:google-services:4.3.4'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
|
@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.vfs.watch=true
|
||||
|
||||
vector.debugPrivateData=false
|
||||
vector.httpLogLevel=NONE
|
||||
vector.httpLogLevel=BASIC
|
||||
|
||||
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
|
||||
#vector.debugPrivateData=true
|
||||
|
@ -36,9 +36,10 @@ android {
|
||||
dependencies {
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version"
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||
|
||||
|
@ -21,34 +21,36 @@ import org.matrix.android.sdk.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> {
|
||||
fun <T> singleBuilder(builder: (MatrixCallback<T>) -> Cancelable): Single<T> = Single.create { emitter ->
|
||||
val callback = object : MatrixCallback<T> {
|
||||
override fun onSuccess(data: T) {
|
||||
it.onSuccess(data)
|
||||
// Add `!!` to fix the warning:
|
||||
// "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon"
|
||||
emitter.onSuccess(data!!)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
it.tryOnError(failure)
|
||||
emitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
val cancelable = builder(callback)
|
||||
it.setCancellable {
|
||||
emitter.setCancellable {
|
||||
cancelable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
|
||||
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
|
||||
fun <T> completableBuilder(builder: (MatrixCallback<T>) -> Cancelable): Completable = Completable.create { emitter ->
|
||||
val callback = object : MatrixCallback<T> {
|
||||
override fun onSuccess(data: T) {
|
||||
it.onComplete()
|
||||
emitter.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
it.tryOnError(failure)
|
||||
emitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
val cancelable = builder(callback)
|
||||
it.setCancellable {
|
||||
emitter.setCancellable {
|
||||
cancelable.cancel()
|
||||
}
|
||||
}
|
||||
|
@ -17,14 +17,20 @@
|
||||
package org.matrix.android.sdk.rx
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.rx2.rxCompletable
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
@ -32,11 +38,6 @@ import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
@ -121,28 +122,28 @@ class RxRoom(private val room: Room) {
|
||||
room.invite3pid(threePid, it)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||
room.updateTopic(topic, it)
|
||||
fun updateTopic(topic: String): Completable = rxCompletable {
|
||||
room.updateTopic(topic)
|
||||
}
|
||||
|
||||
fun updateName(name: String): Completable = completableBuilder<Unit> {
|
||||
room.updateName(name, it)
|
||||
fun updateName(name: String): Completable = rxCompletable {
|
||||
room.updateName(name)
|
||||
}
|
||||
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
|
||||
room.updateHistoryReadability(readability, it)
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable {
|
||||
room.updateHistoryReadability(readability)
|
||||
}
|
||||
|
||||
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = completableBuilder<Unit> {
|
||||
room.updateJoinRule(joinRules, guestAccess, it)
|
||||
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable {
|
||||
room.updateJoinRule(joinRules, guestAccess)
|
||||
}
|
||||
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||
room.updateAvatar(avatarUri, fileName, it)
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable {
|
||||
room.updateAvatar(avatarUri, fileName)
|
||||
}
|
||||
|
||||
fun deleteAvatar(): Completable = completableBuilder<Unit> {
|
||||
room.deleteAvatar(it)
|
||||
fun deleteAvatar(): Completable = rxCompletable {
|
||||
room.deleteAvatar()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
@ -139,7 +140,7 @@ class RxSession(private val session: Session) {
|
||||
}
|
||||
|
||||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
|
||||
searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
|
||||
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "io.realm:realm-gradle-plugin:10.0.0"
|
||||
classpath "io.realm:realm-gradle-plugin:10.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ android {
|
||||
|
||||
release {
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC"
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +125,6 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
@ -146,7 +145,7 @@ dependencies {
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
|
||||
// Image
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
||||
|
||||
// Database
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
||||
|
@ -17,13 +17,13 @@
|
||||
package org.matrix.android.sdk.internal.crypto.encryption
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
@ -57,13 +57,14 @@ class EncryptionTest : InstrumentedTest {
|
||||
@Test
|
||||
fun test_EncryptionStateEvent() {
|
||||
performTest(roomShouldBeEncrypted = true) { room ->
|
||||
// Send an encryption Event as a State Event
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = null,
|
||||
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent(),
|
||||
callback = NoOpMatrixCallback()
|
||||
)
|
||||
runBlocking {
|
||||
// Send an encryption Event as a State Event
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = null,
|
||||
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertNotNull(decryption)
|
||||
// - Check decryptKeyBackupData() returns stg
|
||||
val sessionData = keysBackup
|
||||
.decryptKeyBackupData(keyBackupData!!,
|
||||
.decryptKeyBackupData(keyBackupData,
|
||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
||||
cryptoTestData.roomId,
|
||||
decryption!!)
|
||||
|
@ -111,7 +111,7 @@ class KeysBackupTestHelper(
|
||||
Assert.assertTrue(keysBackup.isEnabled)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
|
||||
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class UrlsExtractorTest : InstrumentedTest {
|
||||
|
||||
private val urlsExtractor = UrlsExtractor()
|
||||
|
||||
@Test
|
||||
fun wrongEventTypeTest() {
|
||||
createEvent(body = "https://matrix.org")
|
||||
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.size shouldBeEqualTo 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneUrlTest() {
|
||||
createEvent(body = "https://matrix.org")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
result[0] shouldBeEqualTo "https://matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withoutProtocolTest() {
|
||||
createEvent(body = "www.matrix.org")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.size shouldBeEqualTo 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneUrlWithParamTest() {
|
||||
createEvent(body = "https://matrix.org?foo=bar")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
result[0] shouldBeEqualTo "https://matrix.org?foo=bar"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneUrlWithParamsTest() {
|
||||
createEvent(body = "https://matrix.org?foo=bar&bar=foo")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneUrlInlinedTest() {
|
||||
createEvent(body = "Hello https://matrix.org, how are you?")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
result[0] shouldBeEqualTo "https://matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoUrlsTest() {
|
||||
createEvent(body = "https://matrix.org https://example.org")
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 2
|
||||
result[0] shouldBeEqualTo "https://matrix.org"
|
||||
result[1] shouldBeEqualTo "https://example.org"
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEvent(body: String): Event = Event(
|
||||
type = EventType.MESSAGE,
|
||||
content = MessageTextContent(
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
body = body
|
||||
).toContent()
|
||||
)
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.network.interceptors
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
|
||||
*/
|
||||
@Synchronized
|
||||
override fun log(@NonNull message: String) {
|
||||
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.v(message)
|
||||
Timber.v(message)
|
||||
|
||||
if (message.startsWith("{")) {
|
||||
// JSON Detected
|
||||
try {
|
||||
val o = JSONObject(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally this is not a JSON string...
|
||||
Timber.e(e)
|
||||
}
|
||||
} else if (message.startsWith("[")) {
|
||||
// JSON Array detected
|
||||
try {
|
||||
val o = JSONArray(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally not JSON...
|
||||
Timber.e(e)
|
||||
}
|
||||
if (message.startsWith("{")) {
|
||||
// JSON Detected
|
||||
try {
|
||||
val o = JSONObject(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally this is not a JSON string...
|
||||
Timber.e(e)
|
||||
}
|
||||
} else if (message.startsWith("[")) {
|
||||
// JSON Array detected
|
||||
try {
|
||||
val o = JSONArray(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally not JSON...
|
||||
Timber.e(e)
|
||||
}
|
||||
// Else not a json string to log
|
||||
}
|
||||
// Else not a json string to log
|
||||
}
|
||||
|
||||
private fun logJson(formattedJson: String) {
|
||||
|
@ -14,16 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.raw
|
||||
package org.matrix.android.sdk.api.cache
|
||||
|
||||
sealed class RawCacheStrategy {
|
||||
sealed class CacheStrategy {
|
||||
// Data is always fetched from the server
|
||||
object NoCache : RawCacheStrategy()
|
||||
object NoCache : CacheStrategy()
|
||||
|
||||
// Once data is retrieved, it is stored for the provided amount of time.
|
||||
// In case of error, and if strict is set to false, the cache can be returned if available
|
||||
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : RawCacheStrategy()
|
||||
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy()
|
||||
|
||||
// Once retrieved, the data is stored in cache and will be always get from the cache
|
||||
object InfiniteCache : RawCacheStrategy()
|
||||
object InfiniteCache : CacheStrategy()
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
|
||||
package org.matrix.android.sdk.api.raw
|
||||
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
|
||||
/**
|
||||
* Useful methods to fetch raw data from the server. The access token will not be used to fetched the data
|
||||
*/
|
||||
@ -23,7 +25,7 @@ interface RawService {
|
||||
/**
|
||||
* Get a URL, either from cache or from the remote server, depending on the cache strategy
|
||||
*/
|
||||
suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String
|
||||
suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String
|
||||
|
||||
/**
|
||||
* Specific case for the well-known file. Cache validity is 8 hours
|
||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityService
|
||||
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||
@ -181,6 +182,11 @@ interface Session :
|
||||
*/
|
||||
fun widgetService(): WidgetService
|
||||
|
||||
/**
|
||||
* Returns the media service associated with the session
|
||||
*/
|
||||
fun mediaService(): MediaService
|
||||
|
||||
/**
|
||||
* Returns the integration manager service associated with the session
|
||||
*/
|
||||
|
@ -21,6 +21,7 @@ import android.os.Parcelable
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
@ -45,5 +46,5 @@ data class ContentAttachmentData(
|
||||
VIDEO
|
||||
}
|
||||
|
||||
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
|
||||
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
||||
}
|
||||
|
@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@ -27,23 +31,6 @@ import java.io.File
|
||||
*/
|
||||
interface FileService {
|
||||
|
||||
enum class DownloadMode {
|
||||
/**
|
||||
* Download file in external storage
|
||||
*/
|
||||
TO_EXPORT,
|
||||
|
||||
/**
|
||||
* Download file in cache
|
||||
*/
|
||||
FOR_INTERNAL_USE,
|
||||
|
||||
/**
|
||||
* Download file in file provider path
|
||||
*/
|
||||
FOR_EXTERNAL_SHARE
|
||||
}
|
||||
|
||||
enum class FileState {
|
||||
IN_CACHE,
|
||||
DOWNLOADING,
|
||||
@ -54,34 +41,79 @@ interface FileService {
|
||||
* Download a file.
|
||||
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
||||
*/
|
||||
fun downloadFile(
|
||||
downloadMode: DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable
|
||||
fun downloadFile(fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable
|
||||
|
||||
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean
|
||||
fun downloadFile(messageContent: MessageWithAttachmentContent,
|
||||
callback: MatrixCallback<File>): Cancelable =
|
||||
downloadFile(
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
url = messageContent.getFileUrl(),
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
callback = callback
|
||||
)
|
||||
|
||||
fun isFileInCache(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?
|
||||
): Boolean
|
||||
|
||||
fun isFileInCache(messageContent: MessageWithAttachmentContent) =
|
||||
isFileInCache(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
|
||||
/**
|
||||
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
* (if not other app won't be able to access it)
|
||||
*/
|
||||
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri?
|
||||
fun getTemporarySharableURI(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Uri?
|
||||
|
||||
fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? =
|
||||
getTemporarySharableURI(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
|
||||
)
|
||||
|
||||
/**
|
||||
* Get information on the given file.
|
||||
* Mimetype should be the same one as passed to downloadFile (limitation for now)
|
||||
*/
|
||||
fun fileState(mxcUrl: String, mimeType: String?): FileState
|
||||
fun fileState(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): FileState
|
||||
|
||||
fun fileState(messageContent: MessageWithAttachmentContent): FileState =
|
||||
fileState(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
|
||||
)
|
||||
|
||||
/**
|
||||
* Clears all the files downloaded by the service
|
||||
* Clears all the files downloaded by the service, including decrypted files
|
||||
*/
|
||||
fun clearCache()
|
||||
|
||||
/**
|
||||
* Clears all the decrypted files by the service
|
||||
*/
|
||||
fun clearDecryptedCache()
|
||||
|
||||
/**
|
||||
* Get size of cached files
|
||||
*/
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.media
|
||||
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
|
||||
interface MediaService {
|
||||
/**
|
||||
* Extract URLs from an Event.
|
||||
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data
|
||||
*/
|
||||
fun extractUrls(event: Event): List<String>
|
||||
|
||||
/**
|
||||
* Get Raw Url Preview data from the homeserver. There is no cache management for this request
|
||||
* @param url The url to get the preview data from
|
||||
* @param timestamp The optional timestamp
|
||||
*/
|
||||
suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict
|
||||
|
||||
/**
|
||||
* Get Url Preview data from the homeserver, or from cache, depending on the cache strategy
|
||||
* @param url The url to get the preview data from
|
||||
* @param timestamp The optional timestamp. Note that this parameter is not taken into account
|
||||
* if the data is already in cache and the cache strategy allow to use it
|
||||
* @param cacheStrategy the cache strategy, see the type for more details
|
||||
*/
|
||||
suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData
|
||||
|
||||
/**
|
||||
* Clear the cache of all retrieved UrlPreview data
|
||||
*/
|
||||
suspend fun clearCache()
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.media
|
||||
|
||||
/**
|
||||
* Facility data class to get the common field of a PreviewUrl response form the server
|
||||
*
|
||||
* Example of return data for the url `https://matrix.org`:
|
||||
* <pre>
|
||||
* {
|
||||
* "matrix:image:size": 112805,
|
||||
* "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
|
||||
* "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
|
||||
* "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
|
||||
* "og:image:height": 467,
|
||||
* "og:image:type": "image/jpeg",
|
||||
* "og:image:width": 911,
|
||||
* "og:locale": "en_US",
|
||||
* "og:site_name": "Matrix.org",
|
||||
* "og:title": "Matrix.org",
|
||||
* "og:type": "website",
|
||||
* "og:url": "https://matrix.org"
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
data class PreviewUrlData(
|
||||
// Value of field "og:url". If not provided, this is the value passed in parameter
|
||||
val url: String,
|
||||
// Value of field "og:site_name"
|
||||
val siteName: String?,
|
||||
// Value of field "og:title"
|
||||
val title: String?,
|
||||
// Value of field "og:description"
|
||||
val description: String?,
|
||||
// Value of field "og:image"
|
||||
val mxcUrl: String?
|
||||
)
|
@ -25,6 +25,7 @@ interface PermalinkService {
|
||||
|
||||
companion object {
|
||||
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
||||
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@ -120,7 +123,7 @@ interface RoomService {
|
||||
*/
|
||||
fun getRoomIdByAlias(roomAlias: String,
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
|
||||
|
||||
/**
|
||||
* Delete a room alias
|
||||
@ -163,4 +166,16 @@ interface RoomService {
|
||||
* @return a LiveData of the optional found room member
|
||||
*/
|
||||
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
|
||||
|
||||
/**
|
||||
* Get some state events about a room
|
||||
*/
|
||||
fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>)
|
||||
|
||||
/**
|
||||
* Use this if you want to get information from a room that you are not yet in (or invited)
|
||||
* It might be possible to get some information on this room if it is public or if guest access is allowed
|
||||
* This call will try to gather some information on this room, but it could fail and get nothing more
|
||||
*/
|
||||
fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@ -54,5 +55,5 @@ data class MessageImageContent(
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
) : MessageImageInfoContent {
|
||||
override val mimeType: String?
|
||||
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*"
|
||||
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.peeking
|
||||
|
||||
sealed class PeekResult {
|
||||
data class Success(
|
||||
val roomId: String,
|
||||
val alias: String?,
|
||||
val name: String?,
|
||||
val topic: String?,
|
||||
val avatarUrl: String?,
|
||||
val numJoinedMembers: Int?,
|
||||
val viaServers: List<String>
|
||||
) : PeekResult()
|
||||
|
||||
data class PeekingNotAllowed(
|
||||
val roomId: String,
|
||||
val alias: String?,
|
||||
val viaServers: List<String>
|
||||
) : PeekResult()
|
||||
|
||||
object UnknownAlias : PeekResult()
|
||||
}
|
@ -18,13 +18,11 @@ package org.matrix.android.sdk.api.session.room.state
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
@ -33,41 +31,41 @@ interface StateService {
|
||||
/**
|
||||
* Update the topic of the room
|
||||
*/
|
||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateTopic(topic: String)
|
||||
|
||||
/**
|
||||
* Update the name of the room
|
||||
*/
|
||||
fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateName(name: String)
|
||||
|
||||
/**
|
||||
* Update the canonical alias of the room
|
||||
* @param alias the canonical alias, or null to reset the canonical alias of this room
|
||||
* @param altAliases the alternative aliases for this room. It should include the canonical alias if any.
|
||||
*/
|
||||
fun updateCanonicalAlias(alias: String?, altAliases: List<String>, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateCanonicalAlias(alias: String?, altAliases: List<String>)
|
||||
|
||||
/**
|
||||
* Update the history readability of the room
|
||||
*/
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateHistoryReadability(readability: RoomHistoryVisibility)
|
||||
|
||||
/**
|
||||
* Update the join rule and/or the guest access
|
||||
*/
|
||||
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?)
|
||||
|
||||
/**
|
||||
* Update the avatar of the room
|
||||
*/
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateAvatar(avatarUri: Uri, fileName: String)
|
||||
|
||||
/**
|
||||
* Delete the avatar of the room
|
||||
*/
|
||||
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun deleteAvatar()
|
||||
|
||||
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict)
|
||||
|
||||
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
||||
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.util
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
// The Android SDK does not provide constant for mime type, add some of them here
|
||||
object MimeTypes {
|
||||
const val Any: String = "*/*"
|
||||
const val OctetStream = "application/octet-stream"
|
||||
|
||||
const val Images = "image/*"
|
||||
|
||||
const val Png = "image/png"
|
||||
const val BadJpg = "image/jpg"
|
||||
const val Jpeg = "image/jpeg"
|
||||
const val Gif = "image/gif"
|
||||
|
||||
fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
|
||||
|
||||
fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
|
||||
fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse()
|
||||
fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse()
|
||||
}
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||
import org.matrix.olm.OlmSAS
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Represents an ongoing short code interactive key verification between two devices.
|
||||
@ -344,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||
}
|
||||
|
||||
protected fun hashUsingAgreedHashMethod(toHash: String): String? {
|
||||
if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) {
|
||||
if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) {
|
||||
val olmUtil = OlmUtility()
|
||||
val hashBytes = olmUtil.sha256(toHash)
|
||||
olmUtil.releaseUtility()
|
||||
@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||
}
|
||||
|
||||
private fun macUsingAgreedMethod(message: String, info: String): String? {
|
||||
if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
|
||||
return getSAS().calculateMacLongKdf(message, info)
|
||||
} else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
|
||||
return getSAS().calculateMac(message, info)
|
||||
return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) {
|
||||
SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
|
||||
SAS_MAC_SHA256 -> getSAS().calculateMac(message, info)
|
||||
else -> null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getDecimalCodeRepresentation(): String {
|
||||
|
@ -20,6 +20,7 @@ import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -27,7 +28,7 @@ import javax.inject.Inject
|
||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 5L
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 6L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
if (oldVersion <= 2) migrateTo3(realm)
|
||||
if (oldVersion <= 3) migrateTo4(realm)
|
||||
if (oldVersion <= 4) migrateTo5(realm)
|
||||
if (oldVersion <= 5) migrateTo6(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
?.removeField("adminE2EByDefault")
|
||||
?.removeField("preferredJitsiDomain")
|
||||
}
|
||||
|
||||
private fun migrateTo6(realm: DynamicRealm) {
|
||||
Timber.d("Step 5 -> 6")
|
||||
realm.schema.create("PreviewUrlCacheEntity")
|
||||
.addField(PreviewUrlCacheEntityFields.URL, String::class.java)
|
||||
.setRequired(PreviewUrlCacheEntityFields.URL, true)
|
||||
.addPrimaryKey(PreviewUrlCacheEntityFields.URL)
|
||||
.addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
|
||||
.addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
|
||||
.addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
|
||||
.addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
|
||||
.addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
|
||||
.addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class PreviewUrlCacheEntity(
|
||||
@PrimaryKey
|
||||
var url: String = "",
|
||||
|
||||
var urlFromServer: String? = null,
|
||||
var siteName: String? = null,
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var mxcUrl: String? = null,
|
||||
|
||||
var lastUpdatedTimestamp: Long = 0L
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
|
||||
PushRulesEntity::class,
|
||||
PushRuleEntity::class,
|
||||
PushConditionEntity::class,
|
||||
PreviewUrlCacheEntity::class,
|
||||
PusherEntity::class,
|
||||
PusherDataEntity::class,
|
||||
ReadReceiptsSummaryEntity::class,
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database.query
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
|
||||
|
||||
/**
|
||||
* Get the current PreviewUrlCacheEntity, return null if it does not exist
|
||||
*/
|
||||
internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? {
|
||||
return realm.where<PreviewUrlCacheEntity>()
|
||||
.equalTo(PreviewUrlCacheEntityFields.URL, url)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current PreviewUrlCacheEntity, create one if it does not exist
|
||||
*/
|
||||
internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity {
|
||||
return get(realm, url) ?: realm.createObject(url)
|
||||
}
|
@ -71,9 +71,6 @@ internal interface MatrixComponent {
|
||||
@CacheDirectory
|
||||
fun cacheDir(): File
|
||||
|
||||
@ExternalFilesDirectory
|
||||
fun externalFilesDir(): File?
|
||||
|
||||
fun olmManager(): OlmManager
|
||||
|
||||
fun taskExecutor(): TaskExecutor
|
||||
|
@ -57,13 +57,6 @@ internal object MatrixModule {
|
||||
return context.cacheDir
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@ExternalFilesDirectory
|
||||
fun providesExternalFilesDir(context: Context): File? {
|
||||
return context.getExternalFilesDir(null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@MatrixScope
|
||||
|
@ -16,14 +16,15 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.network
|
||||
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.internal.network.ssl.CertUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.internal.network.ssl.CertUtil
|
||||
import retrofit2.Call
|
||||
import retrofit2.awaitResponse
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
||||
@ -49,6 +50,9 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||
throw response.toFailure(eventBus)
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
// Log some details about the request which has failed
|
||||
Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}")
|
||||
|
||||
// Check if this is a certificateException
|
||||
CertUtil.getCertificateException(exception)
|
||||
// TODO Support certificate error once logged
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.raw
|
||||
|
||||
import org.matrix.android.sdk.api.raw.RawCacheStrategy
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor(
|
||||
private val getUrlTask: GetUrlTask,
|
||||
private val cleanRawCacheTask: CleanRawCacheTask
|
||||
) : RawService {
|
||||
override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
|
||||
return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
|
||||
override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String {
|
||||
return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy))
|
||||
}
|
||||
|
||||
override suspend fun getWellknown(userId: String): String {
|
||||
val homeServerDomain = userId.substringAfter(":")
|
||||
return getUrl(
|
||||
"https://$homeServerDomain/.well-known/matrix/client",
|
||||
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
||||
CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import okhttp3.ResponseBody
|
||||
import org.matrix.android.sdk.api.raw.RawCacheStrategy
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
|
||||
import org.matrix.android.sdk.internal.database.query.get
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
@ -32,7 +32,7 @@ import javax.inject.Inject
|
||||
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
|
||||
data class Params(
|
||||
val url: String,
|
||||
val rawCacheStrategy: RawCacheStrategy
|
||||
val cacheStrategy: CacheStrategy
|
||||
)
|
||||
}
|
||||
|
||||
@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor(
|
||||
) : GetUrlTask {
|
||||
|
||||
override suspend fun execute(params: GetUrlTask.Params): String {
|
||||
return when (params.rawCacheStrategy) {
|
||||
RawCacheStrategy.NoCache -> doRequest(params.url)
|
||||
is RawCacheStrategy.TtlCache -> doRequestWithCache(
|
||||
return when (params.cacheStrategy) {
|
||||
CacheStrategy.NoCache -> doRequest(params.url)
|
||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||
params.url,
|
||||
params.rawCacheStrategy.validityDurationInMillis,
|
||||
params.rawCacheStrategy.strict
|
||||
params.cacheStrategy.validityDurationInMillis,
|
||||
params.cacheStrategy.strict
|
||||
)
|
||||
RawCacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||
CacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||
params.url,
|
||||
Long.MAX_VALUE,
|
||||
true
|
@ -21,6 +21,10 @@ import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.FileProvider
|
||||
import arrow.core.Try
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.di.CacheDirectory
|
||||
import org.matrix.android.sdk.internal.di.ExternalFilesDirectory
|
||||
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
|
||||
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.md5
|
||||
import org.matrix.android.sdk.internal.util.toCancelable
|
||||
import org.matrix.android.sdk.internal.util.writeToFile
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultFileService @Inject constructor(
|
||||
private val context: Context,
|
||||
@CacheDirectory
|
||||
private val cacheDirectory: File,
|
||||
@ExternalFilesDirectory
|
||||
private val externalFilesDirectory: File?,
|
||||
@SessionDownloadsDirectory
|
||||
private val sessionCacheDirectory: File,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : FileService {
|
||||
|
||||
private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName())
|
||||
// Legacy folder, will be deleted
|
||||
private val legacyFolder = File(sessionCacheDirectory, "MF")
|
||||
// Folder to store downloaded files (not decrypted)
|
||||
private val downloadFolder = File(sessionCacheDirectory, "F")
|
||||
// Folder to store decrypted files
|
||||
private val decryptedFolder = File(downloadFolder, "D")
|
||||
|
||||
private val downloadFolder = File(sessionCacheDirectory, "MF")
|
||||
init {
|
||||
// Clear the legacy downloaded files
|
||||
legacyFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retain ongoing downloads to avoid re-downloading and already downloading file
|
||||
@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor(
|
||||
* Download file in the cache folder, and eventually decrypt it
|
||||
* TODO looks like files are copied 3 times
|
||||
*/
|
||||
override fun downloadFile(downloadMode: FileService.DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
override fun downloadFile(fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable {
|
||||
val unwrappedUrl = url ?: return NoOpCancellable.also {
|
||||
url ?: return NoOpCancellable.also {
|
||||
callback.onFailure(IllegalArgumentException("url is null"))
|
||||
}
|
||||
|
||||
Timber.v("## FileService downloadFile $unwrappedUrl")
|
||||
Timber.v("## FileService downloadFile $url")
|
||||
|
||||
synchronized(ongoing) {
|
||||
val existing = ongoing[unwrappedUrl]
|
||||
val existing = ongoing[url]
|
||||
if (existing != null) {
|
||||
Timber.v("## FileService downloadFile is already downloading.. ")
|
||||
existing.add(callback)
|
||||
return NoOpCancellable
|
||||
} else {
|
||||
// mark as tracked
|
||||
ongoing[unwrappedUrl] = ArrayList()
|
||||
ongoing[url] = ArrayList()
|
||||
// and proceed to download
|
||||
}
|
||||
}
|
||||
@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
|
||||
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
Try {
|
||||
if (!downloadFolder.exists()) {
|
||||
downloadFolder.mkdirs()
|
||||
if (!decryptedFolder.exists()) {
|
||||
decryptedFolder.mkdirs()
|
||||
}
|
||||
// ensure we use unique file name by using URL (mapped to suitable file name)
|
||||
// Also we need to add extension for the FileProvider, if not it lot's of app that it's
|
||||
// shared with will not function well (even if mime type is passed in the intent)
|
||||
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType))
|
||||
}.flatMap { destFile ->
|
||||
if (!destFile.exists()) {
|
||||
getFiles(url, fileName, mimeType, elementToDecrypt != null)
|
||||
}.flatMap { cachedFiles ->
|
||||
if (!cachedFiles.file.exists()) {
|
||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
||||
|
||||
val request = Request.Builder()
|
||||
@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
|
||||
|
||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||
|
||||
if (elementToDecrypt != null) {
|
||||
Timber.v("## FileService: decrypt file")
|
||||
val decryptSuccess = destFile.outputStream().buffered().use {
|
||||
MXEncryptedAttachments.decryptAttachment(
|
||||
source.inputStream(),
|
||||
elementToDecrypt,
|
||||
it
|
||||
)
|
||||
}
|
||||
response.close()
|
||||
if (!decryptSuccess) {
|
||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||
}
|
||||
} else {
|
||||
writeToFile(source.inputStream(), destFile)
|
||||
response.close()
|
||||
}
|
||||
// Write the file to cache (encrypted version if the file is encrypted)
|
||||
writeToFile(source.inputStream(), cachedFiles.file)
|
||||
response.close()
|
||||
} else {
|
||||
Timber.v("## FileService: cache hit for $url")
|
||||
}
|
||||
|
||||
Try.just(copyFile(destFile, downloadMode))
|
||||
Try.just(cachedFiles)
|
||||
}
|
||||
}.fold({
|
||||
callback.onFailure(it)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[unwrappedUrl]?.also {
|
||||
ongoing.remove(unwrappedUrl)
|
||||
}.flatMap { cachedFiles ->
|
||||
// Decrypt if necessary
|
||||
if (cachedFiles.decryptedFile != null) {
|
||||
if (!cachedFiles.decryptedFile.exists()) {
|
||||
Timber.v("## FileService: decrypt file")
|
||||
// Ensure the parent folder exists
|
||||
cachedFiles.decryptedFile.parentFile?.mkdirs()
|
||||
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
|
||||
cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
|
||||
MXEncryptedAttachments.decryptAttachment(
|
||||
inputStream,
|
||||
elementToDecrypt,
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!decryptSuccess) {
|
||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||
}
|
||||
} else {
|
||||
Timber.v("## FileService: cache hit for decrypted file")
|
||||
}
|
||||
Try.just(cachedFiles.decryptedFile)
|
||||
} else {
|
||||
// Clear file
|
||||
Try.just(cachedFiles.file)
|
||||
}
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onFailure(it) }
|
||||
}
|
||||
}, { file ->
|
||||
callback.onSuccess(file)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[unwrappedUrl]?.also {
|
||||
ongoing.remove(unwrappedUrl)
|
||||
}.fold(
|
||||
{ throwable ->
|
||||
callback.onFailure(throwable)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[url]?.also {
|
||||
ongoing.remove(url)
|
||||
}
|
||||
}
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onFailure(throwable) }
|
||||
}
|
||||
},
|
||||
{ file ->
|
||||
callback.onSuccess(file)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[url]?.also {
|
||||
ongoing.remove(url)
|
||||
}
|
||||
}
|
||||
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onSuccess(file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onSuccess(file) }
|
||||
}
|
||||
})
|
||||
)
|
||||
}.toCancelable()
|
||||
}
|
||||
|
||||
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) {
|
||||
val file = File(downloadFolder, fileForUrl(url, mimeType))
|
||||
val source = inputStream.source().buffer()
|
||||
file.sink().buffer().let { sink ->
|
||||
source.use { input ->
|
||||
sink.use { output ->
|
||||
output.writeAll(input)
|
||||
fun storeDataFor(mxcUrl: String,
|
||||
filename: String?,
|
||||
mimeType: String?,
|
||||
originalFile: File,
|
||||
encryptedFile: File?) {
|
||||
val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
|
||||
if (encryptedFile != null) {
|
||||
// We switch the two files here, original file it the decrypted file
|
||||
files.decryptedFile?.let { originalFile.copyTo(it) }
|
||||
encryptedFile.copyTo(files.file)
|
||||
} else {
|
||||
// Just copy the original file
|
||||
originalFile.copyTo(files.file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun safeFileName(fileName: String?, mimeType: String?): String {
|
||||
return buildString {
|
||||
// filename has to be safe for the Android System
|
||||
val result = fileName
|
||||
?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: DEFAULT_FILENAME
|
||||
append(result)
|
||||
// Check that the extension is correct regarding the mimeType
|
||||
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
||||
if (extensionFromMime != null) {
|
||||
// Compare
|
||||
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
|
||||
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
|
||||
// Missing extension, or diff in extension, add the one provided by the mimetype
|
||||
append(".")
|
||||
append(extensionFromMime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileForUrl(url: String, mimeType: String?): String {
|
||||
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
||||
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName()
|
||||
override fun isFileInCache(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Boolean {
|
||||
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
|
||||
}
|
||||
|
||||
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean {
|
||||
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists()
|
||||
internal data class CachedFiles(
|
||||
// This is the downloaded file. Can be clear or encrypted
|
||||
val file: File,
|
||||
// This is the decrypted file. Null if the original file is not encrypted
|
||||
val decryptedFile: File?
|
||||
) {
|
||||
fun getClearFile(): File = decryptedFile ?: file
|
||||
}
|
||||
|
||||
override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState {
|
||||
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE
|
||||
private fun getFiles(mxcUrl: String,
|
||||
fileName: String?,
|
||||
mimeType: String?,
|
||||
isEncrypted: Boolean): CachedFiles {
|
||||
val hashFolder = mxcUrl.md5()
|
||||
val safeFileName = safeFileName(fileName, mimeType)
|
||||
return if (isEncrypted) {
|
||||
// Encrypted file
|
||||
CachedFiles(
|
||||
File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"),
|
||||
File(decryptedFolder, "$hashFolder/$safeFileName")
|
||||
)
|
||||
} else {
|
||||
// Clear file
|
||||
CachedFiles(
|
||||
File(downloadFolder, "$hashFolder/$safeFileName"),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fileState(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
|
||||
mxcUrl ?: return FileService.FileState.UNKNOWN
|
||||
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
|
||||
val isDownloading = synchronized(ongoing) {
|
||||
ongoing[mxcUrl] != null
|
||||
}
|
||||
@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor(
|
||||
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
* (if not other app won't be able to access it)
|
||||
*/
|
||||
override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? {
|
||||
override fun getTemporarySharableURI(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Uri? {
|
||||
mxcUrl ?: return null
|
||||
// this string could be extracted no?
|
||||
val authority = "${context.packageName}.mx-sdk.fileprovider"
|
||||
val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType))
|
||||
val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile()
|
||||
if (!targetFile.exists()) return null
|
||||
return FileProvider.getUriForFile(context, authority, targetFile)
|
||||
}
|
||||
|
||||
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
||||
// TODO some of this seems outdated, will need to be re-worked
|
||||
return when (downloadMode) {
|
||||
FileService.DownloadMode.TO_EXPORT ->
|
||||
file.copyTo(File(externalFilesDirectory, file.name), true)
|
||||
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
|
||||
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
|
||||
FileService.DownloadMode.FOR_INTERNAL_USE ->
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCacheSize(): Int {
|
||||
return downloadFolder.walkTopDown()
|
||||
.onEnter {
|
||||
@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
|
||||
override fun clearCache() {
|
||||
downloadFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
override fun clearDecryptedCache() {
|
||||
decryptedFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENCRYPTED_FILENAME = "encrypted.bin"
|
||||
// The extension would be added from the mimetype
|
||||
private const val DEFAULT_FILENAME = "file"
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.group.GroupService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||
@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor(
|
||||
private val permalinkService: Lazy<PermalinkService>,
|
||||
private val secureStorageService: Lazy<SecureStorageService>,
|
||||
private val profileService: Lazy<ProfileService>,
|
||||
private val mediaService: Lazy<MediaService>,
|
||||
private val widgetService: Lazy<WidgetService>,
|
||||
private val syncThreadProvider: Provider<SyncThread>,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor(
|
||||
|
||||
override fun widgetService(): WidgetService = widgetService.get()
|
||||
|
||||
override fun mediaService(): MediaService = mediaService.get()
|
||||
|
||||
override fun integrationManagerService() = integrationManagerService
|
||||
|
||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||
|
@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule
|
||||
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
|
||||
import org.matrix.android.sdk.internal.session.identity.IdentityModule
|
||||
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
|
||||
import org.matrix.android.sdk.internal.session.media.MediaModule
|
||||
import org.matrix.android.sdk.internal.session.openid.OpenIdModule
|
||||
import org.matrix.android.sdk.internal.session.profile.ProfileModule
|
||||
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
||||
@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
GroupModule::class,
|
||||
ContentModule::class,
|
||||
CacheModule::class,
|
||||
MediaModule::class,
|
||||
CryptoModule::class,
|
||||
PushersModule::class,
|
||||
OpenIdModule::class,
|
||||
|
@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||
import org.matrix.android.sdk.internal.di.Authenticated
|
||||
import org.matrix.android.sdk.internal.di.CacheDirectory
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||
@ -169,9 +170,9 @@ internal abstract class SessionModule {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionDownloadsDirectory
|
||||
fun providesCacheDir(@SessionId sessionId: String,
|
||||
context: Context): File {
|
||||
return File(context.cacheDir, "downloads/$sessionId")
|
||||
fun providesDownloadsCacheDir(@SessionId sessionId: String,
|
||||
@CacheDirectory cacheFile: File): File {
|
||||
return File(cacheFile, "downloads/$sessionId")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -20,6 +20,9 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContentUploadResponse(
|
||||
internal data class ContentUploadResponse(
|
||||
/**
|
||||
* Required. The MXC URI to the uploaded content.
|
||||
*/
|
||||
@Json(name = "content_uri") val contentUri: String
|
||||
)
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@ -58,7 +59,7 @@ internal object ThumbnailExtractor {
|
||||
height = thumbnailHeight,
|
||||
size = thumbnailSize.toLong(),
|
||||
bytes = outputStream.toByteArray(),
|
||||
mimeType = "image/jpeg"
|
||||
mimeType = MimeTypes.Jpeg
|
||||
)
|
||||
thumbnail.recycle()
|
||||
outputStream.reset()
|
||||
|
@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
params.attachment.size
|
||||
)
|
||||
|
||||
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
|
||||
if (attachment.type == ContentAttachmentData.Type.IMAGE
|
||||
// Do not compress gif
|
||||
&& attachment.mimeType != MimeTypes.Gif
|
||||
&& params.compressBeforeSending) {
|
||||
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
||||
.also { compressedFile ->
|
||||
// Get new Bitmap size
|
||||
@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedFile: File?
|
||||
val contentUploadResponse = if (params.isEncrypted) {
|
||||
Timber.v("## FileService: Encrypt file")
|
||||
|
||||
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
.also { filesToDelete.add(it) }
|
||||
|
||||
uploadedFileEncryptedFileInfo =
|
||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
|
||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
|
||||
notifyTracker(params) {
|
||||
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
|
||||
}
|
||||
@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
Timber.v("## FileService: Uploading file")
|
||||
|
||||
fileUploader
|
||||
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
|
||||
.uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
|
||||
} else {
|
||||
Timber.v("## FileService: Clear file")
|
||||
encryptedFile = null
|
||||
fileUploader
|
||||
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||
}
|
||||
|
||||
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
|
||||
try {
|
||||
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
||||
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
||||
}
|
||||
fileService.storeDataFor(
|
||||
mxcUrl = contentUploadResponse.contentUri,
|
||||
filename = params.attachment.name,
|
||||
mimeType = params.attachment.getSafeMimeType(),
|
||||
originalFile = workingFile,
|
||||
encryptedFile = encryptedFile
|
||||
)
|
||||
Timber.v("## FileService: cache storage updated")
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## FileService: Failed to update file cache")
|
||||
@ -252,7 +262,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
|
||||
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
"thumb_${params.attachment.name}",
|
||||
"application/octet-stream",
|
||||
MimeTypes.OctetStream,
|
||||
thumbnailProgressListener)
|
||||
UploadThumbnailResult(
|
||||
contentUploadResponse.contentUri,
|
||||
|
@ -22,19 +22,12 @@ import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
|
||||
internal interface CapabilitiesAPI {
|
||||
|
||||
/**
|
||||
* Request the homeserver capabilities
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
||||
fun getCapabilities(): Call<GetCapabilitiesResult>
|
||||
|
||||
/**
|
||||
* Request the upload capabilities
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
||||
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
|
||||
|
||||
/**
|
||||
* Request the versions
|
||||
*/
|
||||
|
@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
|
||||
import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult
|
||||
import org.matrix.android.sdk.internal.session.media.MediaAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
|
||||
@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
private val capabilitiesAPI: CapabilitiesAPI,
|
||||
private val mediaAPI: MediaAPI,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val eventBus: EventBus,
|
||||
private val getWellknownTask: GetWellknownTask,
|
||||
@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
val uploadCapabilities = runCatching {
|
||||
executeRequest<GetUploadCapabilitiesResult>(eventBus) {
|
||||
apiCall = capabilitiesAPI.getUploadCapabilities()
|
||||
val mediaConfig = runCatching {
|
||||
executeRequest<GetMediaConfigResult>(eventBus) {
|
||||
apiCall = mediaAPI.getMediaConfig()
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
||||
}.getOrNull()
|
||||
|
||||
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
|
||||
insertInDb(capabilities, mediaConfig, versions, wellknownResult)
|
||||
}
|
||||
|
||||
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
|
||||
getUploadCapabilitiesResult: GetUploadCapabilitiesResult?,
|
||||
getMediaConfigResult: GetMediaConfigResult?,
|
||||
getVersionResult: Versions?,
|
||||
getWellknownResult: WellknownResult?) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
||||
}
|
||||
|
||||
if (getUploadCapabilitiesResult != null) {
|
||||
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
||||
if (getMediaConfigResult != null) {
|
||||
homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize
|
||||
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ClearPreviewUrlCacheTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultClearPreviewUrlCacheTask @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy
|
||||
) : ClearPreviewUrlCacheTask {
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
realm.where<PreviewUrlCacheEntity>()
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.util.getOrPut
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultMediaService @Inject constructor(
|
||||
private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask,
|
||||
private val getPreviewUrlTask: GetPreviewUrlTask,
|
||||
private val getRawPreviewUrlTask: GetRawPreviewUrlTask,
|
||||
private val urlsExtractor: UrlsExtractor
|
||||
) : MediaService {
|
||||
// Cache of extracted URLs
|
||||
private val extractedUrlsCache = LruCache<String, List<String>>(1_000)
|
||||
|
||||
override fun extractUrls(event: Event): List<String> {
|
||||
return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
|
||||
}
|
||||
|
||||
private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}"
|
||||
|
||||
override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict {
|
||||
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))
|
||||
}
|
||||
|
||||
override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData {
|
||||
return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy))
|
||||
}
|
||||
|
||||
override suspend fun clearCache() {
|
||||
extractedUrlsCache.evictAll()
|
||||
clearPreviewUrlCacheTask.execute(Unit)
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
* 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
|
||||
* 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,
|
||||
@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.homeserver
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class GetUploadCapabilitiesResult(
|
||||
internal data class GetMediaConfigResult(
|
||||
/**
|
||||
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
|
||||
* If not listed or null, the size limit should be treated as unknown.
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||
import org.matrix.android.sdk.internal.database.query.get
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetPreviewUrlTask : Task<GetPreviewUrlTask.Params, PreviewUrlData> {
|
||||
data class Params(
|
||||
val url: String,
|
||||
val timestamp: Long?,
|
||||
val cacheStrategy: CacheStrategy
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultGetPreviewUrlTask @Inject constructor(
|
||||
private val mediaAPI: MediaAPI,
|
||||
private val eventBus: EventBus,
|
||||
@SessionDatabase private val monarchy: Monarchy
|
||||
) : GetPreviewUrlTask {
|
||||
|
||||
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
||||
return when (params.cacheStrategy) {
|
||||
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||
params.url,
|
||||
params.timestamp,
|
||||
params.cacheStrategy.validityDurationInMillis,
|
||||
params.cacheStrategy.strict
|
||||
)
|
||||
CacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||
params.url,
|
||||
params.timestamp,
|
||||
Long.MAX_VALUE,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData {
|
||||
return executeRequest<JsonDict>(eventBus) {
|
||||
apiCall = mediaAPI.getPreviewUrlData(url, timestamp)
|
||||
}
|
||||
.toPreviewUrlData(url)
|
||||
}
|
||||
|
||||
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
|
||||
return PreviewUrlData(
|
||||
url = (get("og:url") as? String) ?: url,
|
||||
siteName = get("og:site_name") as? String,
|
||||
title = get("og:title") as? String,
|
||||
description = get("og:description") as? String,
|
||||
mxcUrl = get("og:image") as? String
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData {
|
||||
// Get data from cache
|
||||
var dataFromCache: PreviewUrlData? = null
|
||||
var isCacheValid = false
|
||||
monarchy.doWithRealm { realm ->
|
||||
val entity = PreviewUrlCacheEntity.get(realm, url)
|
||||
dataFromCache = entity?.toDomain()
|
||||
isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis
|
||||
}
|
||||
|
||||
val finalDataFromCache = dataFromCache
|
||||
if (finalDataFromCache != null && isCacheValid) {
|
||||
return finalDataFromCache
|
||||
}
|
||||
|
||||
// No cache or outdated cache
|
||||
val data = try {
|
||||
doRequest(url, timestamp)
|
||||
} catch (throwable: Throwable) {
|
||||
// In case of error, we can return value from cache even if outdated
|
||||
return finalDataFromCache
|
||||
?.takeIf { !strict }
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
// Store cache
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url)
|
||||
previewUrlCacheEntity.urlFromServer = data.url
|
||||
previewUrlCacheEntity.siteName = data.siteName
|
||||
previewUrlCacheEntity.title = data.title
|
||||
previewUrlCacheEntity.description = data.description
|
||||
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
||||
|
||||
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetRawPreviewUrlTask : Task<GetRawPreviewUrlTask.Params, JsonDict> {
|
||||
data class Params(
|
||||
val url: String,
|
||||
val timestamp: Long?
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultGetRawPreviewUrlTask @Inject constructor(
|
||||
private val mediaAPI: MediaAPI,
|
||||
private val eventBus: EventBus
|
||||
) : GetRawPreviewUrlTask {
|
||||
|
||||
override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict {
|
||||
return executeRequest(eventBus) {
|
||||
apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
internal interface MediaAPI {
|
||||
/**
|
||||
* Retrieve the configuration of the content repository
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
||||
fun getMediaConfig(): Call<GetMediaConfigResult>
|
||||
|
||||
/**
|
||||
* Get information about a URL for the client. Typically this is called when a client
|
||||
* sees a URL in a message and wants to render a preview for the user.
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-url
|
||||
* @param url Required. The URL to get a preview of.
|
||||
* @param ts The preferred point in time to return a preview for. The server may return a newer version
|
||||
* if it does not have the requested version available.
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url")
|
||||
fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call<JsonDict>
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
internal abstract class MediaModule {
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@SessionScope
|
||||
fun providesMediaAPI(retrofit: Retrofit): MediaAPI {
|
||||
return retrofit.create(MediaAPI::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindMediaService(service: DefaultMediaService): MediaService
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||
|
||||
/**
|
||||
* PreviewUrlCacheEntity -> PreviewUrlData
|
||||
*/
|
||||
internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
||||
url = urlFromServer ?: url,
|
||||
siteName = siteName,
|
||||
title = title,
|
||||
description = description,
|
||||
mxcUrl = mxcUrl
|
||||
)
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
import android.util.Patterns
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class UrlsExtractor @Inject constructor() {
|
||||
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
|
||||
private val urlRegex = Patterns.WEB_URL.toRegex()
|
||||
|
||||
fun extract(event: Event): List<String> {
|
||||
return event.takeIf { it.getClearType() == EventType.MESSAGE }
|
||||
?.getClearContent()
|
||||
?.toModel<MessageContent>()
|
||||
?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_EMOTE }
|
||||
?.body
|
||||
?.let { urlRegex.findAll(it) }
|
||||
?.map { it.value }
|
||||
?.filter { it.startsWith("https://") || it.startsWith("http://") }
|
||||
?.distinct()
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
|
||||
@ -80,7 +81,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
||||
|
||||
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
|
||||
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
|
||||
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
|
||||
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
|
||||
userStore.updateAvatar(userId, response.contentUri)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
@ -35,10 +37,13 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
|
||||
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
|
||||
@ -55,6 +60,8 @@ internal class DefaultRoomService @Inject constructor(
|
||||
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
|
||||
private val roomIdByAliasTask: GetRoomIdByAliasTask,
|
||||
private val deleteRoomAliasTask: DeleteRoomAliasTask,
|
||||
private val resolveRoomStateTask: ResolveRoomStateTask,
|
||||
private val peekRoomTask: PeekRoomTask,
|
||||
private val roomGetter: RoomGetter,
|
||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||
@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor(
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable {
|
||||
return roomIdByAliasTask
|
||||
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
|
||||
this.callback = callback
|
||||
@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor(
|
||||
results.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) {
|
||||
resolveRoomStateTask
|
||||
.configureWith(ResolveRoomStateTask.Params(roomId)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) {
|
||||
peekRoomTask
|
||||
.configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ internal interface RoomAPI {
|
||||
@Body body: ThreePidInviteBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Send a generic state events
|
||||
* Send a generic state event
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param stateEventType the state event type
|
||||
@ -195,7 +195,7 @@ internal interface RoomAPI {
|
||||
@Body params: JsonDict): Call<Unit>
|
||||
|
||||
/**
|
||||
* Send a generic state events
|
||||
* Send a generic state event
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param stateEventType the state event type
|
||||
@ -208,6 +208,13 @@ internal interface RoomAPI {
|
||||
@Path("state_key") stateKey: String,
|
||||
@Body params: JsonDict): Call<Unit>
|
||||
|
||||
/**
|
||||
* Get state events of a room
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
|
||||
fun getRoomState(@Path("roomId") roomId: String) : Call<List<Event>>
|
||||
|
||||
/**
|
||||
* Send a relation event to a room.
|
||||
*
|
||||
|
@ -57,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe
|
||||
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
|
||||
@ -223,4 +227,10 @@ internal abstract class RoomModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
|
||||
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<RoomAliasDescription>> {
|
||||
data class Params(
|
||||
val roomAlias: String,
|
||||
val searchOnServer: Boolean
|
||||
@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
|
||||
private val eventBus: EventBus
|
||||
) : GetRoomIdByAliasTask {
|
||||
|
||||
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
|
||||
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<RoomAliasDescription> {
|
||||
val roomId = Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
|
||||
}
|
||||
return if (roomId != null) {
|
||||
Optional.from(roomId)
|
||||
Optional.from(RoomAliasDescription(roomId))
|
||||
} else if (!params.searchOnServer) {
|
||||
Optional.from<String>(null)
|
||||
Optional.from(null)
|
||||
} else {
|
||||
roomId = tryOrNull("## Failed to get roomId from alias") {
|
||||
val description = tryOrNull("## Failed to get roomId from alias") {
|
||||
executeRequest<RoomAliasDescription>(eventBus) {
|
||||
apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias)
|
||||
}
|
||||
}?.roomId
|
||||
Optional.from(roomId)
|
||||
}
|
||||
Optional.from(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RoomAliasDescription(
|
||||
data class RoomAliasDescription(
|
||||
/**
|
||||
* The room ID for this alias.
|
||||
*/
|
||||
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
||||
@ -96,7 +97,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||
fileUploader.uploadFromUri(
|
||||
uri = avatarUri,
|
||||
filename = UUID.randomUUID().toString(),
|
||||
mimeType = "image/jpeg")
|
||||
mimeType = MimeTypes.Jpeg)
|
||||
}
|
||||
?.let { response ->
|
||||
Event(
|
||||
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.peeking
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface PeekRoomTask : Task<PeekRoomTask.Params, PeekResult> {
|
||||
data class Params(
|
||||
val roomIdOrAlias: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultPeekRoomTask @Inject constructor(
|
||||
private val getRoomIdByAliasTask: GetRoomIdByAliasTask,
|
||||
private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask,
|
||||
private val getPublicRoomTask: GetPublicRoomTask,
|
||||
private val resolveRoomStateTask: ResolveRoomStateTask
|
||||
) : PeekRoomTask {
|
||||
|
||||
override suspend fun execute(params: PeekRoomTask.Params): PeekResult {
|
||||
val roomId: String
|
||||
val serverList: List<String>
|
||||
val isAlias = MatrixPatterns.isRoomAlias(params.roomIdOrAlias)
|
||||
if (isAlias) {
|
||||
// get alias description
|
||||
val aliasDescription = getRoomIdByAliasTask
|
||||
.execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true))
|
||||
.getOrNull()
|
||||
?: return PeekResult.UnknownAlias
|
||||
|
||||
roomId = aliasDescription.roomId
|
||||
serverList = aliasDescription.servers
|
||||
} else {
|
||||
roomId = params.roomIdOrAlias
|
||||
serverList = emptyList()
|
||||
}
|
||||
|
||||
// Is it a public room?
|
||||
val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) {
|
||||
RoomDirectoryVisibility.PRIVATE -> {
|
||||
// We cannot resolve this room :/
|
||||
null
|
||||
}
|
||||
RoomDirectoryVisibility.PUBLIC -> {
|
||||
// Try to find it in directory
|
||||
val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1))
|
||||
else null
|
||||
|
||||
getPublicRoomTask.execute(GetPublicRoomTask.Params(
|
||||
server = serverList.firstOrNull(),
|
||||
publicRoomsParams = PublicRoomsParams(
|
||||
filter = filter,
|
||||
limit = 20.takeIf { filter != null } ?: 100
|
||||
)
|
||||
)).chunk?.firstOrNull { it.roomId == roomId }
|
||||
}
|
||||
}
|
||||
|
||||
if (publicRepoResult != null) {
|
||||
return PeekResult.Success(
|
||||
roomId = roomId,
|
||||
alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias },
|
||||
avatarUrl = publicRepoResult.avatarUrl,
|
||||
name = publicRepoResult.name,
|
||||
topic = publicRepoResult.topic,
|
||||
numJoinedMembers = publicRepoResult.numJoinedMembers,
|
||||
viaServers = serverList
|
||||
)
|
||||
}
|
||||
|
||||
// mm... try to peek state ? maybe the room is not public but yet allow guest to get events?
|
||||
// this could be slow
|
||||
try {
|
||||
val stateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId))
|
||||
val name = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_NAME && it.stateKey == "" }
|
||||
?.let { it.content?.toModel<RoomNameContent>()?.name }
|
||||
|
||||
val topic = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_TOPIC && it.stateKey == "" }
|
||||
?.let { it.content?.toModel<RoomTopicContent>()?.topic }
|
||||
|
||||
val avatarUrl = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_AVATAR }
|
||||
?.let { it.content?.toModel<RoomAvatarContent>()?.avatarUrl }
|
||||
|
||||
val alias = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
|
||||
?.let { it.content?.toModel<RoomCanonicalAliasContent>()?.canonicalAlias }
|
||||
|
||||
// not sure if it's the right way to do that :/
|
||||
val memberCount = stateEvents
|
||||
.filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true }
|
||||
.distinctBy { it.stateKey }
|
||||
.count()
|
||||
|
||||
return PeekResult.Success(
|
||||
roomId = roomId,
|
||||
alias = alias,
|
||||
avatarUrl = avatarUrl,
|
||||
name = name,
|
||||
topic = topic,
|
||||
numJoinedMembers = memberCount,
|
||||
viaServers = serverList
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
// Would be M_FORBIDDEN if cannot peek :/
|
||||
// User XXX not in room !XXX, and room previews are disabled
|
||||
return PeekResult.PeekingNotAllowed(
|
||||
roomId = roomId,
|
||||
alias = params.roomIdOrAlias.takeIf { isAlias },
|
||||
viaServers = serverList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.peeking
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ResolveRoomStateTask : Task<ResolveRoomStateTask.Params, List<Event>> {
|
||||
data class Params(
|
||||
val roomId: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultResolveRoomStateTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus
|
||||
) : ResolveRoomStateTask {
|
||||
|
||||
override suspend fun execute(params: ResolveRoomStateTask.Params): List<Event> {
|
||||
return executeRequest(eventBus) {
|
||||
apiCall = roomAPI.getRoomState(params.roomId)
|
||||
}
|
||||
}
|
||||
}
|
@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.info!!.size,
|
||||
mimeType = messageContent.info.mimeType!!,
|
||||
name = messageContent.body,
|
||||
name = messageContent.getFileName(),
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.FILE
|
||||
)
|
||||
@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
|
||||
override fun cancelSend(eventId: String) {
|
||||
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
|
||||
// This is maybe the current task, so cancel it too
|
||||
eventSenderProcessor.cancel(eventId, roomId)
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.deleteFailedEcho(roomId, eventId)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
@ -106,17 +107,21 @@ internal class EventSenderProcessor @Inject constructor(
|
||||
// non blocking add to queue
|
||||
sendingQueue.add(task)
|
||||
markAsManaged(task)
|
||||
return object : Cancelable {
|
||||
override fun cancel() {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
fun cancel(eventId: String, roomId: String) {
|
||||
(currentTask as? SendEventQueuedTask)
|
||||
?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId }
|
||||
?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||
}
|
||||
|
||||
private var currentTask: QueuedTask? = null
|
||||
|
||||
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
|
||||
|
||||
private var networkAvailableLock = Object()
|
||||
@ -129,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor(
|
||||
while (!isInterrupted) {
|
||||
Timber.v("## SendThread wait for task to process")
|
||||
val task = sendingQueue.take()
|
||||
.also { currentTask = it }
|
||||
Timber.v("## SendThread Found task to process $task")
|
||||
|
||||
if (task.isCancelled()) {
|
||||
@ -183,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor(
|
||||
task.onTaskFailed()
|
||||
throw InterruptedException()
|
||||
}
|
||||
exception is CancellationException -> {
|
||||
Timber.v("## SendThread task has been cancelled")
|
||||
break@retryLoop
|
||||
}
|
||||
else -> {
|
||||
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
|
||||
// this task is in error, check next one?
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import android.content.Context
|
||||
import org.matrix.android.sdk.api.auth.data.sessionId
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
@ -16,14 +16,26 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
abstract class QueuedTask {
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
abstract class QueuedTask : Cancelable {
|
||||
var retryCount = 0
|
||||
|
||||
abstract suspend fun execute()
|
||||
private var hasBeenCancelled: Boolean = false
|
||||
|
||||
suspend fun execute() {
|
||||
if (!isCancelled()) {
|
||||
doExecute()
|
||||
}
|
||||
}
|
||||
|
||||
abstract suspend fun doExecute()
|
||||
|
||||
abstract fun onTaskFailed()
|
||||
|
||||
abstract fun isCancelled() : Boolean
|
||||
open fun isCancelled() = hasBeenCancelled
|
||||
|
||||
abstract fun cancel()
|
||||
final override fun cancel() {
|
||||
hasBeenCancelled = true
|
||||
}
|
||||
}
|
||||
|
@ -22,20 +22,18 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
|
||||
internal class RedactQueuedTask(
|
||||
val toRedactEventId: String,
|
||||
private val toRedactEventId: String,
|
||||
val redactionLocalEchoId: String,
|
||||
val roomId: String,
|
||||
val reason: String?,
|
||||
val redactEventTask: RedactEventTask,
|
||||
val localEchoRepository: LocalEchoRepository,
|
||||
val cancelSendTracker: CancelSendTracker
|
||||
private val roomId: String,
|
||||
private val reason: String?,
|
||||
private val redactEventTask: RedactEventTask,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val cancelSendTracker: CancelSendTracker
|
||||
) : QueuedTask() {
|
||||
|
||||
private var _isCancelled: Boolean = false
|
||||
override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]"
|
||||
|
||||
override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]"
|
||||
|
||||
override suspend fun execute() {
|
||||
override suspend fun doExecute() {
|
||||
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
|
||||
}
|
||||
|
||||
@ -44,10 +42,6 @@ internal class RedactQueuedTask(
|
||||
}
|
||||
|
||||
override fun isCancelled(): Boolean {
|
||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
_isCancelled = true
|
||||
return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +33,9 @@ internal class SendEventQueuedTask(
|
||||
val cancelSendTracker: CancelSendTracker
|
||||
) : QueuedTask() {
|
||||
|
||||
private var _isCancelled: Boolean = false
|
||||
override fun toString() = "[SendEventQueuedTask ${event.eventId}]"
|
||||
|
||||
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
|
||||
|
||||
override suspend fun execute() {
|
||||
override suspend fun doExecute() {
|
||||
sendEventTask.execute(SendEventTask.Params(event, encrypt))
|
||||
}
|
||||
|
||||
@ -56,10 +54,6 @@ internal class SendEventQueuedTask(
|
||||
}
|
||||
|
||||
override fun isCancelled(): Boolean {
|
||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
_isCancelled = true
|
||||
return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
@ -32,22 +31,15 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||
import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
|
||||
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val fileUploader: FileUploader,
|
||||
private val addRoomAliasTask: AddRoomAliasTask
|
||||
) : StateService {
|
||||
@ -73,45 +65,38 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
||||
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
|
||||
}
|
||||
|
||||
override fun sendStateEvent(
|
||||
override suspend fun sendStateEvent(
|
||||
eventType: String,
|
||||
stateKey: String?,
|
||||
body: JsonDict,
|
||||
callback: MatrixCallback<Unit>
|
||||
): Cancelable {
|
||||
body: JsonDict
|
||||
) {
|
||||
val params = SendStateTask.Params(
|
||||
roomId = roomId,
|
||||
stateKey = stateKey,
|
||||
eventType = eventType,
|
||||
body = body
|
||||
)
|
||||
return sendStateTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
sendStateTask.execute(params)
|
||||
}
|
||||
|
||||
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
override suspend fun updateTopic(topic: String) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_TOPIC,
|
||||
body = mapOf("topic" to topic),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
override suspend fun updateName(name: String) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_NAME,
|
||||
body = mapOf("name" to name),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateCanonicalAlias(alias: String?, altAliases: List<String>, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
override suspend fun updateCanonicalAlias(alias: String?, altAliases: List<String>) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
body = RoomCanonicalAliasContent(
|
||||
canonicalAlias = alias,
|
||||
@ -123,64 +108,48 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
||||
// Sort for the cleanup
|
||||
.sorted()
|
||||
).toContent(),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
override suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
body = mapOf("history_visibility" to readability),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
if (joinRules != null) {
|
||||
awaitCallback<Unit> {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_JOIN_RULES,
|
||||
body = RoomJoinRulesContent(joinRules).toContent(),
|
||||
callback = it,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
if (guestAccess != null) {
|
||||
awaitCallback<Unit> {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
body = RoomGuestAccessContent(guestAccess).toContent(),
|
||||
callback = it,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) {
|
||||
if (joinRules != null) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_JOIN_RULES,
|
||||
body = RoomJoinRulesContent(joinRules).toContent(),
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
if (guestAccess != null) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
body = RoomGuestAccessContent(guestAccess).toContent(),
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg")
|
||||
awaitCallback<Unit> {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = mapOf("url" to response.contentUri),
|
||||
callback = it,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
override suspend fun updateAvatar(avatarUri: Uri, fileName: String) {
|
||||
val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg)
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = mapOf("url" to response.contentUri),
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
override suspend fun deleteAvatar() {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = emptyMap(),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
|
||||
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.android.sdk.internal.util.Debouncer
|
||||
import org.matrix.android.sdk.internal.util.createUIHandler
|
||||
@ -50,14 +49,13 @@ private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
||||
|
||||
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
private val typingUsersTracker: DefaultTypingUsersTracker,
|
||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||
private val activeCallHandler: ActiveCallHandler
|
||||
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
|
||||
private var state: SyncState = SyncState.Idle
|
||||
private var liveState = MutableLiveData<SyncState>(state)
|
||||
private var liveState = MutableLiveData(state)
|
||||
private val lock = Object()
|
||||
private val syncScope = CoroutineScope(SupervisorJob())
|
||||
private val debouncer = Debouncer(createUIHandler())
|
||||
@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
return
|
||||
}
|
||||
state = newState
|
||||
debouncer.debounce("post_state", Runnable {
|
||||
debouncer.debounce("post_state", {
|
||||
liveState.value = newState
|
||||
}, 150)
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ import java.io.InputStream
|
||||
*/
|
||||
@WorkerThread
|
||||
fun writeToFile(inputStream: InputStream, outputFile: File) {
|
||||
// Ensure the parent folder exists, else it will crash
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
outputFile.outputStream().use {
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package org.matrix.android.sdk.internal.util
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Compute a Hash of a String, using md5 algorithm
|
||||
@ -26,7 +27,7 @@ fun String.md5() = try {
|
||||
digest.update(toByteArray())
|
||||
digest.digest()
|
||||
.joinToString("") { String.format("%02X", it) }
|
||||
.toLowerCase()
|
||||
.toLowerCase(Locale.ROOT)
|
||||
} catch (exc: Exception) {
|
||||
// Should not happen, but just in case
|
||||
hashCode().toString()
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.util
|
||||
|
||||
import androidx.collection.LruCache
|
||||
|
||||
@Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
|
||||
internal inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||
return get(key) ?: defaultValue().also { put(key, it) }
|
||||
}
|
@ -43,8 +43,8 @@ android {
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.0'
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
||||
|
||||
// Log
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
|
||||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===83
|
||||
enum class===84
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
@ -315,9 +315,8 @@ dependencies {
|
||||
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.fragment:fragment:$fragment_version"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
|
||||
@ -362,11 +361,11 @@ dependencies {
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
|
||||
// Pref
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
|
||||
// UI
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha04'
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:html:$markwon_version"
|
||||
@ -374,7 +373,7 @@ dependencies {
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
implementation 'com.google.android:flexbox:1.1.1'
|
||||
implementation "androidx.autofill:autofill:$autofill_version"
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10'
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||
|
||||
// Custom Tab
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
@ -418,7 +417,7 @@ dependencies {
|
||||
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
|
||||
|
||||
// gplay flavor only
|
||||
gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') {
|
||||
gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
@ -441,6 +440,10 @@ dependencies {
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.13'
|
||||
|
||||
// Emoji Keyboard
|
||||
implementation 'com.vanniktech:emoji-material:0.7.0'
|
||||
implementation 'com.vanniktech:emoji-google:0.7.0'
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
|
@ -41,6 +41,7 @@
|
||||
<issue id="ObsoleteSdkInt" severity="error" />
|
||||
<issue id="Recycle" severity="error" />
|
||||
<issue id="KotlinPropertyAccess" severity="error" />
|
||||
<issue id="DefaultLocale" severity="error" />
|
||||
|
||||
<issue id="InvalidPackage">
|
||||
<!-- Ignore error from HtmlCompressor lib -->
|
||||
@ -52,6 +53,9 @@
|
||||
<!-- Manifest -->
|
||||
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
||||
|
||||
<!-- Dependencies -->
|
||||
<issue id="KtxExtensionAvailable" severity="error" />
|
||||
|
||||
<!-- Timber -->
|
||||
<!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
|
||||
<!-- <issue id="BinaryOperationInTimber" severity="error" />-->
|
||||
|
@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.startAddGoogleAccountIntent
|
||||
@ -36,29 +36,33 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi
|
||||
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
status = TestStatus.RUNNING
|
||||
try {
|
||||
FirebaseInstanceId.getInstance().instanceId
|
||||
FirebaseMessaging.getInstance().token
|
||||
.addOnCompleteListener(context) { task ->
|
||||
if (!task.isSuccessful) {
|
||||
val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage
|
||||
// Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
|
||||
if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) {
|
||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
|
||||
} else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) {
|
||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
|
||||
} else if ("ACCOUNT_MISSING".equals(errorMsg)) {
|
||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
|
||||
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
|
||||
override fun doFix() {
|
||||
startAddGoogleAccountIntent(context, activityResultLauncher)
|
||||
}
|
||||
description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") {
|
||||
"SERVICE_NOT_AVAILABLE" -> {
|
||||
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
|
||||
}
|
||||
"TOO_MANY_REGISTRATIONS" -> {
|
||||
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
|
||||
}
|
||||
"ACCOUNT_MISSING" -> {
|
||||
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
|
||||
override fun doFix() {
|
||||
startAddGoogleAccountIntent(context, activityResultLauncher)
|
||||
}
|
||||
}
|
||||
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
|
||||
}
|
||||
else -> {
|
||||
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
|
||||
}
|
||||
} else {
|
||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
|
||||
}
|
||||
status = TestStatus.FAILED
|
||||
} else {
|
||||
task.result?.token?.let { token ->
|
||||
val tok = token.substring(0, Math.min(8, token.length)) + "********************"
|
||||
task.result?.let { token ->
|
||||
val tok = token.take(8) + "********************"
|
||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
|
||||
Timber.e("Retrieved FCM token success [$tok].")
|
||||
// Ensure it is well store in our local storage
|
||||
|
@ -21,7 +21,7 @@ import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.DefaultSharedPreferences
|
||||
@ -71,14 +71,16 @@ object FcmHelper {
|
||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
if (checkPlayServices(activity)) {
|
||||
try {
|
||||
FirebaseInstanceId.getInstance().instanceId
|
||||
.addOnSuccessListener(activity) { instanceIdResult ->
|
||||
storeFcmToken(activity, instanceIdResult.token)
|
||||
FirebaseMessaging.getInstance().token
|
||||
.addOnSuccessListener { token ->
|
||||
storeFcmToken(activity, token)
|
||||
if (registerPusher) {
|
||||
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
|
||||
pushersManager.registerPusherWithFcmKey(token)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") }
|
||||
.addOnFailureListener { e ->
|
||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
||||
}
|
||||
|
@ -81,8 +81,9 @@
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".features.home.HomeActivity"
|
||||
android:launchMode="singleTask"/>
|
||||
<activity
|
||||
android:name=".features.home.HomeActivity"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".features.login.LoginActivity"
|
||||
android:launchMode="singleTask"
|
||||
@ -190,15 +191,25 @@
|
||||
<activity
|
||||
android:name=".features.signout.soft.SoftLogoutActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity android:name=".features.permalink.PermalinkHandlerActivity" android:launchMode="singleTask">
|
||||
<activity
|
||||
android:name=".features.permalink.PermalinkHandlerActivity"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="matrix.to" />
|
||||
<data
|
||||
android:host="user"
|
||||
android:scheme="element" />
|
||||
<data
|
||||
android:host="room"
|
||||
android:scheme="element" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
@ -347,11 +347,6 @@ SOFTWARE.
|
||||
<br/>
|
||||
Copyright 2017 Gabriel Ittner.
|
||||
</li>
|
||||
<li>
|
||||
<b>Android-multipicker-library</b>
|
||||
<br/>
|
||||
Copyright 2018 Kumar Bibek
|
||||
</li>
|
||||
<li>
|
||||
<b>htmlcompressor</b>
|
||||
<br/>
|
||||
@ -390,6 +385,11 @@ SOFTWARE.
|
||||
<br/>
|
||||
Copyright 2018, Aleksandr Nikiforov
|
||||
</li>
|
||||
<li>
|
||||
<b>Emoji</b>
|
||||
<br/>
|
||||
Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.gabrielittner.threetenbp.LazyThreeTen
|
||||
import com.vanniktech.emoji.EmojiManager
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.DaggerVectorComponent
|
||||
import im.vector.app.core.di.HasVectorInjector
|
||||
@ -184,6 +186,8 @@ class VectorApplication :
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
})
|
||||
|
||||
EmojiManager.install(GoogleEmojiProvider())
|
||||
}
|
||||
|
||||
private fun enableStrictModeIfNeeded() {
|
||||
|
@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@ -110,11 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
|
||||
}
|
||||
// Use the file vector service, will avoid flickering and redownload after upload
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
mimeType = data.mimeType,
|
||||
id = data.eventId,
|
||||
url = data.url,
|
||||
fileName = data.filename,
|
||||
mimeType = data.mimeType,
|
||||
url = data.url,
|
||||
elementToDecrypt = data.elementToDecrypt,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
|
@ -21,6 +21,7 @@ import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import im.vector.app.core.utils.getFileExtension
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Returns the mimetype from a uri.
|
||||
@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
|
||||
|
||||
if (null != mimeType) {
|
||||
// the mimetype is sometimes in uppercase.
|
||||
mimeType = mimeType.toLowerCase()
|
||||
mimeType = mimeType.toLowerCase(Locale.ROOT)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to open resource input stream")
|
||||
|
@ -43,7 +43,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
|
||||
* so you can use this in a switchMap or a flatMap
|
||||
*/
|
||||
// False positive
|
||||
@Suppress("USELESS_CAST")
|
||||
@Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
|
||||
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
|
||||
setState { stateReducer(Loading()) }
|
||||
return map { Success(it) as Async<T> }
|
||||
@ -56,7 +56,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
|
||||
* so you can use this in a switchMap or a flatMap
|
||||
*/
|
||||
// False positive
|
||||
@Suppress("USELESS_CAST")
|
||||
@Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
|
||||
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
|
||||
setState { stateReducer(Loading()) }
|
||||
return map { Success(it) as Async<T> }
|
||||
|
@ -20,17 +20,11 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import im.vector.app.core.utils.getFileExtension
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Mime types
|
||||
*/
|
||||
const val MIME_TYPE_JPEG = "image/jpeg"
|
||||
const val MIME_TYPE_JPG = "image/jpg"
|
||||
const val MIME_TYPE_IMAGE_ALL = "image/*"
|
||||
const val MIME_TYPE_ALL_CONTENT = "*/*"
|
||||
|
||||
data class Resource(
|
||||
var mContentStream: InputStream? = null,
|
||||
var mMimeType: String? = null
|
||||
@ -55,7 +49,7 @@ data class Resource(
|
||||
* @return true if the opened resource is a jpeg one.
|
||||
*/
|
||||
fun isJpegResource(): Boolean {
|
||||
return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType
|
||||
return mMimeType.normalizeMimeType() == MimeTypes.Jpeg
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.ui.bottomsheet
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import im.vector.app.core.platform.EmptyAction
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
|
||||
abstract class BottomSheetGenericViewModel<State : MvRxState>(initialState: State) :
|
||||
VectorViewModel<State, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
override fun handle(action: EmptyAction) {
|
||||
// No op
|
||||
}
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
/*
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.app.core.ui.views
|
||||
|
@ -44,7 +44,7 @@ open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableD
|
||||
}
|
||||
|
||||
override fun post(value: T) {
|
||||
behaviorRelay.accept(value)
|
||||
behaviorRelay.accept(value!!)
|
||||
}
|
||||
|
||||
private fun createRelay(): BehaviorRelay<T> {
|
||||
@ -68,6 +68,6 @@ open class PublishDataSource<T> : MutableDataSource<T> {
|
||||
}
|
||||
|
||||
override fun post(value: T) {
|
||||
publishRelay.accept(value)
|
||||
publishRelay.accept(value!!)
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,17 @@
|
||||
/*
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.app.core.utils
|
||||
|
||||
|
@ -48,6 +48,10 @@ import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@ -138,7 +142,7 @@ fun openFileSelection(activity: Activity,
|
||||
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection)
|
||||
|
||||
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
fileIntent.type = "*/*"
|
||||
fileIntent.type = MimeTypes.Any
|
||||
|
||||
try {
|
||||
activityResultLauncher
|
||||
@ -182,7 +186,7 @@ fun openCamera(activity: Activity, titlePrefix: String, requestCode: Int): Strin
|
||||
// The Galaxy S not only requires the name of the file to output the image to, but will also not
|
||||
// set the mime type of the picture it just took (!!!). We assume that the Galaxy S takes image/jpegs
|
||||
// so the attachment uploader doesn't freak out about there being no mimetype in the content database.
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, MimeTypes.Jpeg)
|
||||
var dummyUri: Uri? = null
|
||||
try {
|
||||
dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
@ -344,10 +348,10 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
|
||||
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
val externalContentUri = when {
|
||||
mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
val uri = context.contentResolver.insert(externalContentUri, values)
|
||||
@ -365,7 +369,7 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
|
||||
notificationUtils.buildDownloadFileNotification(
|
||||
uri,
|
||||
filename,
|
||||
mediaMimeType ?: "application/octet-stream"
|
||||
mediaMimeType ?: MimeTypes.OctetStream
|
||||
).let { notification ->
|
||||
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
|
||||
}
|
||||
@ -385,10 +389,10 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val dest = when {
|
||||
mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES
|
||||
mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES
|
||||
mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC
|
||||
else -> Environment.DIRECTORY_DOWNLOADS
|
||||
mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES
|
||||
mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES
|
||||
mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC
|
||||
else -> Environment.DIRECTORY_DOWNLOADS
|
||||
}
|
||||
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
|
||||
try {
|
||||
@ -405,7 +409,7 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
|
||||
savedFile.name,
|
||||
title,
|
||||
true,
|
||||
mediaMimeType ?: "application/octet-stream",
|
||||
mediaMimeType ?: MimeTypes.OctetStream,
|
||||
savedFile.absolutePath,
|
||||
savedFile.length(),
|
||||
true)
|
||||
|
@ -19,6 +19,7 @@ package im.vector.app.core.utils
|
||||
import android.content.Context
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
// Implementation should return true in case of success
|
||||
typealias ActionOnFile = (file: File) -> Boolean
|
||||
@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? {
|
||||
val ext = filename.substring(dotPos + 1)
|
||||
|
||||
if (ext.isNotBlank()) {
|
||||
return ext.toLowerCase()
|
||||
return ext.toLowerCase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user