Merge branch 'develop' into feature/ons/fix_hide_state_events

This commit is contained in:
Onuray Sahin 2020-12-14 12:03:49 +03:00 committed by GitHub
commit b321838502
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 2952 additions and 1019 deletions

View file

@ -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>

View file

@ -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)
===================================================

View file

@ -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'

View file

@ -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'

View file

@ -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

View file

@ -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"

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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'

View file

@ -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()
)
}
}
}

View file

@ -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!!)

View file

@ -111,7 +111,7 @@ class KeysBackupTestHelper(
Assert.assertTrue(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
}
/**

View file

@ -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()
)
}

View file

@ -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) {

View file

@ -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()
}

View file

@ -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

View file

@ -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
*/

View file

@ -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()
}

View file

@ -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
*/

View file

@ -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()
}

View file

@ -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?
)

View file

@ -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://"
}
/**

View file

@ -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>)
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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?

View file

@ -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()
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PreviewUrlCacheEntity::class,
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,

View file

@ -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)
}

View file

@ -71,9 +71,6 @@ internal interface MatrixComponent {
@CacheDirectory
fun cacheDir(): File
@ExternalFilesDirectory
fun externalFilesDir(): File?
fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor

View file

@ -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

View file

@ -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

View file

@ -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)
)
}

View file

@ -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

View file

@ -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"
}
}

View 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()

View file

@ -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,

View file

@ -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

View file

@ -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
)

View file

@ -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()

View file

@ -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,

View file

@ -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
*/

View file

@ -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
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

@ -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.

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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>
}

View file

@ -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
}

View file

@ -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
)

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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.
*

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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.
*/

View file

@ -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(

View file

@ -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
)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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?

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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) }
}

View file

@ -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'

View file

@ -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

View file

@ -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"

View file

@ -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" />-->

View file

@ -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

View file

@ -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")
}

View file

@ -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>

View file

@ -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

View file

@ -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() {

View file

@ -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) {

View 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")

View file

@ -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> }

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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!!)
}
}

View file

@ -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

View file

@ -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)

View file

@ -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