mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Merge branch 'develop' into feature/state_service_coroutines
This commit is contained in:
commit
5b74eb3bca
113 changed files with 2175 additions and 689 deletions
|
@ -24,6 +24,8 @@
|
||||||
<w>pbkdf</w>
|
<w>pbkdf</w>
|
||||||
<w>pids</w>
|
<w>pids</w>
|
||||||
<w>pkcs</w>
|
<w>pkcs</w>
|
||||||
|
<w>previewable</w>
|
||||||
|
<w>previewables</w>
|
||||||
<w>riotx</w>
|
<w>riotx</w>
|
||||||
<w>signin</w>
|
<w>signin</w>
|
||||||
<w>signout</w>
|
<w>signout</w>
|
||||||
|
|
10
CHANGES.md
10
CHANGES.md
|
@ -4,30 +4,40 @@ Changes in Element 1.0.12 (2020-XX-XX)
|
||||||
Features ✨:
|
Features ✨:
|
||||||
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
||||||
- Room setting: update join rules and guest access (#2442)
|
- 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 🙌:
|
Improvements 🙌:
|
||||||
- Add Setting Item to Change PIN (#2462)
|
- Add Setting Item to Change PIN (#2462)
|
||||||
- Improve room history visibility setting UX (#1579)
|
- Improve room history visibility setting UX (#1579)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
|
- Fix cancellation of sending event (#2438)
|
||||||
- Double bottomsheet effect after verify with passphrase
|
- Double bottomsheet effect after verify with passphrase
|
||||||
- EditText cursor jumps to the start while typing fast (#2469)
|
- EditText cursor jumps to the start while typing fast (#2469)
|
||||||
|
- Show preview when sending attachment from the keyboard (#2440)
|
||||||
|
- Do not compress GIFs (#1616, #1254)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
- StateService now exposes suspendable function instead of using MatrixCallback.
|
- StateService now exposes suspendable function instead of using MatrixCallback.
|
||||||
|
- RawCacheStrategy has been moved and renamed to CacheStrategy
|
||||||
|
- FileService: remove useless FileService.DownloadMode
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
- Upgrade some dependencies and Kotlin version
|
- Upgrade some dependencies and Kotlin version
|
||||||
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
|
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
|
||||||
|
- Upgrade Realm dependency to 10.1.2
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
-
|
-
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
- Remove "Status.im" theme #2424
|
- 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)
|
Changes in Element 1.0.11 (2020-11-27)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
|
@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
|
||||||
org.gradle.vfs.watch=true
|
org.gradle.vfs.watch=true
|
||||||
|
|
||||||
vector.debugPrivateData=false
|
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
|
# 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
|
#vector.debugPrivateData=true
|
||||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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 {
|
release {
|
||||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||||
assertNotNull(decryption)
|
assertNotNull(decryption)
|
||||||
// - Check decryptKeyBackupData() returns stg
|
// - Check decryptKeyBackupData() returns stg
|
||||||
val sessionData = keysBackup
|
val sessionData = keysBackup
|
||||||
.decryptKeyBackupData(keyBackupData!!,
|
.decryptKeyBackupData(keyBackupData,
|
||||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
session.olmInboundGroupSession!!.sessionIdentifier(),
|
||||||
cryptoTestData.roomId,
|
cryptoTestData.roomId,
|
||||||
decryption!!)
|
decryption!!)
|
||||||
|
|
|
@ -111,7 +111,7 @@ class KeysBackupTestHelper(
|
||||||
Assert.assertTrue(keysBackup.isEnabled)
|
Assert.assertTrue(keysBackup.isEnabled)
|
||||||
|
|
||||||
stateObserver.stopAndCheckStates(null)
|
stateObserver.stopAndCheckStates(null)
|
||||||
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
|
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
internal class UrlsExtractorTest : InstrumentedTest {
|
||||||
|
|
||||||
|
private val urlsExtractor = UrlsExtractor()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongEventTypeTest() {
|
||||||
|
createEvent(body = "https://matrix.org")
|
||||||
|
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.size shouldBeEqualTo 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun oneUrlTest() {
|
||||||
|
createEvent(body = "https://matrix.org")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.let { result ->
|
||||||
|
result.size shouldBeEqualTo 1
|
||||||
|
result[0] shouldBeEqualTo "https://matrix.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withoutProtocolTest() {
|
||||||
|
createEvent(body = "www.matrix.org")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.size shouldBeEqualTo 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun oneUrlWithParamTest() {
|
||||||
|
createEvent(body = "https://matrix.org?foo=bar")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.let { result ->
|
||||||
|
result.size shouldBeEqualTo 1
|
||||||
|
result[0] shouldBeEqualTo "https://matrix.org?foo=bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun oneUrlWithParamsTest() {
|
||||||
|
createEvent(body = "https://matrix.org?foo=bar&bar=foo")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.let { result ->
|
||||||
|
result.size shouldBeEqualTo 1
|
||||||
|
result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun oneUrlInlinedTest() {
|
||||||
|
createEvent(body = "Hello https://matrix.org, how are you?")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.let { result ->
|
||||||
|
result.size shouldBeEqualTo 1
|
||||||
|
result[0] shouldBeEqualTo "https://matrix.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun twoUrlsTest() {
|
||||||
|
createEvent(body = "https://matrix.org https://example.org")
|
||||||
|
.let { urlsExtractor.extract(it) }
|
||||||
|
.let { result ->
|
||||||
|
result.size shouldBeEqualTo 2
|
||||||
|
result[0] shouldBeEqualTo "https://matrix.org"
|
||||||
|
result[1] shouldBeEqualTo "https://example.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEvent(body: String): Event = Event(
|
||||||
|
type = EventType.MESSAGE,
|
||||||
|
content = MessageTextContent(
|
||||||
|
msgType = MessageType.MSGTYPE_TEXT,
|
||||||
|
body = body
|
||||||
|
).toContent()
|
||||||
|
)
|
||||||
|
}
|
|
@ -17,7 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.network.interceptors
|
package org.matrix.android.sdk.internal.network.interceptors
|
||||||
|
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import org.matrix.android.sdk.BuildConfig
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun log(@NonNull message: String) {
|
override fun log(@NonNull message: String) {
|
||||||
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG
|
Timber.v(message)
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Timber.v(message)
|
|
||||||
|
|
||||||
if (message.startsWith("{")) {
|
if (message.startsWith("{")) {
|
||||||
// JSON Detected
|
// JSON Detected
|
||||||
try {
|
try {
|
||||||
val o = JSONObject(message)
|
val o = JSONObject(message)
|
||||||
logJson(o.toString(INDENT_SPACE))
|
logJson(o.toString(INDENT_SPACE))
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
// Finally this is not a JSON string...
|
// Finally this is not a JSON string...
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
} else if (message.startsWith("[")) {
|
} else if (message.startsWith("[")) {
|
||||||
// JSON Array detected
|
// JSON Array detected
|
||||||
try {
|
try {
|
||||||
val o = JSONArray(message)
|
val o = JSONArray(message)
|
||||||
logJson(o.toString(INDENT_SPACE))
|
logJson(o.toString(INDENT_SPACE))
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
// Finally not JSON...
|
// Finally not JSON...
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Else not a json string to log
|
|
||||||
}
|
}
|
||||||
|
// Else not a json string to log
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logJson(formattedJson: String) {
|
private fun logJson(formattedJson: String) {
|
||||||
|
|
|
@ -14,16 +14,16 @@
|
||||||
* limitations under the License.
|
* 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
|
// 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.
|
// 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
|
// 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
|
// Once retrieved, the data is stored in cache and will be always get from the cache
|
||||||
object InfiniteCache : RawCacheStrategy()
|
object InfiniteCache : CacheStrategy()
|
||||||
}
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.raw
|
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
|
* 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
|
* 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
|
* Specific case for the well-known file. Cache validity is 8 hours
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
import org.matrix.android.sdk.api.session.identity.IdentityService
|
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.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.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||||
|
@ -181,6 +182,11 @@ interface Session :
|
||||||
*/
|
*/
|
||||||
fun widgetService(): WidgetService
|
fun widgetService(): WidgetService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the media service associated with the session
|
||||||
|
*/
|
||||||
|
fun mediaService(): MediaService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the integration manager service associated with the session
|
* Returns the integration manager service associated with the session
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Parcelable
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -45,5 +46,5 @@ data class ContentAttachmentData(
|
||||||
VIDEO
|
VIDEO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
|
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
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.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,23 +31,6 @@ import java.io.File
|
||||||
*/
|
*/
|
||||||
interface FileService {
|
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 {
|
enum class FileState {
|
||||||
IN_CACHE,
|
IN_CACHE,
|
||||||
DOWNLOADING,
|
DOWNLOADING,
|
||||||
|
@ -54,34 +41,79 @@ interface FileService {
|
||||||
* Download a file.
|
* 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.
|
* 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(
|
fun downloadFile(fileName: String,
|
||||||
downloadMode: DownloadMode,
|
mimeType: String?,
|
||||||
id: String,
|
url: String?,
|
||||||
fileName: String,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
mimeType: String?,
|
callback: MatrixCallback<File>): Cancelable
|
||||||
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
|
* 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)
|
* (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.
|
* Get information on the given file.
|
||||||
* Mimetype should be the same one as passed to downloadFile (limitation for now)
|
* 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()
|
fun clearCache()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all the decrypted files by the service
|
||||||
|
*/
|
||||||
|
fun clearDecryptedCache()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get size of cached files
|
* Get size of cached files
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.media
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
|
||||||
|
interface MediaService {
|
||||||
|
/**
|
||||||
|
* Extract URLs from an Event.
|
||||||
|
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data
|
||||||
|
*/
|
||||||
|
fun extractUrls(event: Event): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Raw Url Preview data from the homeserver. There is no cache management for this request
|
||||||
|
* @param url The url to get the preview data from
|
||||||
|
* @param timestamp The optional timestamp
|
||||||
|
*/
|
||||||
|
suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Url Preview data from the homeserver, or from cache, depending on the cache strategy
|
||||||
|
* @param url The url to get the preview data from
|
||||||
|
* @param timestamp The optional timestamp. Note that this parameter is not taken into account
|
||||||
|
* if the data is already in cache and the cache strategy allow to use it
|
||||||
|
* @param cacheStrategy the cache strategy, see the type for more details
|
||||||
|
*/
|
||||||
|
suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache of all retrieved UrlPreview data
|
||||||
|
*/
|
||||||
|
suspend fun clearCache()
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.media
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facility data class to get the common field of a PreviewUrl response form the server
|
||||||
|
*
|
||||||
|
* Example of return data for the url `https://matrix.org`:
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "matrix:image:size": 112805,
|
||||||
|
* "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
|
||||||
|
* "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
|
||||||
|
* "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
|
||||||
|
* "og:image:height": 467,
|
||||||
|
* "og:image:type": "image/jpeg",
|
||||||
|
* "og:image:width": 911,
|
||||||
|
* "og:locale": "en_US",
|
||||||
|
* "og:site_name": "Matrix.org",
|
||||||
|
* "og:title": "Matrix.org",
|
||||||
|
* "og:type": "website",
|
||||||
|
* "og:url": "https://matrix.org"
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
data class PreviewUrlData(
|
||||||
|
// Value of field "og:url". If not provided, this is the value passed in parameter
|
||||||
|
val url: String,
|
||||||
|
// Value of field "og:site_name"
|
||||||
|
val siteName: String?,
|
||||||
|
// Value of field "og:title"
|
||||||
|
val title: String?,
|
||||||
|
// Value of field "og:description"
|
||||||
|
val description: String?,
|
||||||
|
// Value of field "og:image"
|
||||||
|
val mxcUrl: String?
|
||||||
|
)
|
|
@ -20,6 +20,7 @@ import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
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.session.room.model.relation.RelationDefaultContent
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -54,5 +55,5 @@ data class MessageImageContent(
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageImageInfoContent {
|
) : MessageImageInfoContent {
|
||||||
override val mimeType: String?
|
override val mimeType: String?
|
||||||
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*"
|
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import io.realm.DynamicRealm
|
||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
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.PendingThreePidEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -27,7 +28,7 @@ import javax.inject.Inject
|
||||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||||
|
|
||||||
companion object {
|
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) {
|
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 <= 2) migrateTo3(realm)
|
||||||
if (oldVersion <= 3) migrateTo4(realm)
|
if (oldVersion <= 3) migrateTo4(realm)
|
||||||
if (oldVersion <= 4) migrateTo5(realm)
|
if (oldVersion <= 4) migrateTo5(realm)
|
||||||
|
if (oldVersion <= 5) migrateTo6(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
|
@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||||
?.removeField("adminE2EByDefault")
|
?.removeField("adminE2EByDefault")
|
||||||
?.removeField("preferredJitsiDomain")
|
?.removeField("preferredJitsiDomain")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateTo6(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 5 -> 6")
|
||||||
|
realm.schema.create("PreviewUrlCacheEntity")
|
||||||
|
.addField(PreviewUrlCacheEntityFields.URL, String::class.java)
|
||||||
|
.setRequired(PreviewUrlCacheEntityFields.URL, true)
|
||||||
|
.addPrimaryKey(PreviewUrlCacheEntityFields.URL)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
|
||||||
|
.addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.database.model
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
|
internal open class PreviewUrlCacheEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
var url: String = "",
|
||||||
|
|
||||||
|
var urlFromServer: String? = null,
|
||||||
|
var siteName: String? = null,
|
||||||
|
var title: String? = null,
|
||||||
|
var description: String? = null,
|
||||||
|
var mxcUrl: String? = null,
|
||||||
|
|
||||||
|
var lastUpdatedTimestamp: Long = 0L
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
|
||||||
PushRulesEntity::class,
|
PushRulesEntity::class,
|
||||||
PushRuleEntity::class,
|
PushRuleEntity::class,
|
||||||
PushConditionEntity::class,
|
PushConditionEntity::class,
|
||||||
|
PreviewUrlCacheEntity::class,
|
||||||
PusherEntity::class,
|
PusherEntity::class,
|
||||||
PusherDataEntity::class,
|
PusherDataEntity::class,
|
||||||
ReadReceiptsSummaryEntity::class,
|
ReadReceiptsSummaryEntity::class,
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.database.query
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current PreviewUrlCacheEntity, return null if it does not exist
|
||||||
|
*/
|
||||||
|
internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? {
|
||||||
|
return realm.where<PreviewUrlCacheEntity>()
|
||||||
|
.equalTo(PreviewUrlCacheEntityFields.URL, url)
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current PreviewUrlCacheEntity, create one if it does not exist
|
||||||
|
*/
|
||||||
|
internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity {
|
||||||
|
return get(realm, url) ?: realm.createObject(url)
|
||||||
|
}
|
|
@ -71,9 +71,6 @@ internal interface MatrixComponent {
|
||||||
@CacheDirectory
|
@CacheDirectory
|
||||||
fun cacheDir(): File
|
fun cacheDir(): File
|
||||||
|
|
||||||
@ExternalFilesDirectory
|
|
||||||
fun externalFilesDir(): File?
|
|
||||||
|
|
||||||
fun olmManager(): OlmManager
|
fun olmManager(): OlmManager
|
||||||
|
|
||||||
fun taskExecutor(): TaskExecutor
|
fun taskExecutor(): TaskExecutor
|
||||||
|
|
|
@ -57,13 +57,6 @@ internal object MatrixModule {
|
||||||
return context.cacheDir
|
return context.cacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@ExternalFilesDirectory
|
|
||||||
fun providesExternalFilesDir(context: Context): File? {
|
|
||||||
return context.getExternalFilesDir(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
|
|
|
@ -16,14 +16,15 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.network
|
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.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.greenrobot.eventbus.EventBus
|
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.Call
|
||||||
import retrofit2.awaitResponse
|
import retrofit2.awaitResponse
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
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)
|
throw response.toFailure(eventBus)
|
||||||
}
|
}
|
||||||
} catch (exception: Throwable) {
|
} 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
|
// Check if this is a certificateException
|
||||||
CertUtil.getCertificateException(exception)
|
CertUtil.getCertificateException(exception)
|
||||||
// TODO Support certificate error once logged
|
// TODO Support certificate error once logged
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.raw
|
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 org.matrix.android.sdk.api.raw.RawService
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor(
|
||||||
private val getUrlTask: GetUrlTask,
|
private val getUrlTask: GetUrlTask,
|
||||||
private val cleanRawCacheTask: CleanRawCacheTask
|
private val cleanRawCacheTask: CleanRawCacheTask
|
||||||
) : RawService {
|
) : RawService {
|
||||||
override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
|
override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String {
|
||||||
return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
|
return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getWellknown(userId: String): String {
|
override suspend fun getWellknown(userId: String): String {
|
||||||
val homeServerDomain = userId.substringAfter(":")
|
val homeServerDomain = userId.substringAfter(":")
|
||||||
return getUrl(
|
return getUrl(
|
||||||
"https://$homeServerDomain/.well-known/matrix/client",
|
"https://$homeServerDomain/.well-known/matrix/client",
|
||||||
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import okhttp3.ResponseBody
|
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.model.RawCacheEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.get
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
@ -32,7 +32,7 @@ import javax.inject.Inject
|
||||||
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
|
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val url: String,
|
val url: String,
|
||||||
val rawCacheStrategy: RawCacheStrategy
|
val cacheStrategy: CacheStrategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor(
|
||||||
) : GetUrlTask {
|
) : GetUrlTask {
|
||||||
|
|
||||||
override suspend fun execute(params: GetUrlTask.Params): String {
|
override suspend fun execute(params: GetUrlTask.Params): String {
|
||||||
return when (params.rawCacheStrategy) {
|
return when (params.cacheStrategy) {
|
||||||
RawCacheStrategy.NoCache -> doRequest(params.url)
|
CacheStrategy.NoCache -> doRequest(params.url)
|
||||||
is RawCacheStrategy.TtlCache -> doRequestWithCache(
|
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
params.rawCacheStrategy.validityDurationInMillis,
|
params.cacheStrategy.validityDurationInMillis,
|
||||||
params.rawCacheStrategy.strict
|
params.cacheStrategy.strict
|
||||||
)
|
)
|
||||||
RawCacheStrategy.InfiniteCache -> doRequestWithCache(
|
CacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
Long.MAX_VALUE,
|
Long.MAX_VALUE,
|
||||||
true
|
true
|
|
@ -21,6 +21,10 @@ import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import arrow.core.Try
|
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.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
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.api.util.NoOpCancellable
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
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.SessionDownloadsDirectory
|
||||||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
|
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.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
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.toCancelable
|
||||||
import org.matrix.android.sdk.internal.util.writeToFile
|
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 timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultFileService @Inject constructor(
|
internal class DefaultFileService @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@CacheDirectory
|
|
||||||
private val cacheDirectory: File,
|
|
||||||
@ExternalFilesDirectory
|
|
||||||
private val externalFilesDirectory: File?,
|
|
||||||
@SessionDownloadsDirectory
|
@SessionDownloadsDirectory
|
||||||
private val sessionCacheDirectory: File,
|
private val sessionCacheDirectory: File,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
|
@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor
|
||||||
) : FileService {
|
) : 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
|
* 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
|
* Download file in the cache folder, and eventually decrypt it
|
||||||
* TODO looks like files are copied 3 times
|
* TODO looks like files are copied 3 times
|
||||||
*/
|
*/
|
||||||
override fun downloadFile(downloadMode: FileService.DownloadMode,
|
override fun downloadFile(fileName: String,
|
||||||
id: String,
|
|
||||||
fileName: String,
|
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
url: String?,
|
url: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
callback: MatrixCallback<File>): Cancelable {
|
callback: MatrixCallback<File>): Cancelable {
|
||||||
val unwrappedUrl = url ?: return NoOpCancellable.also {
|
url ?: return NoOpCancellable.also {
|
||||||
callback.onFailure(IllegalArgumentException("url is null"))
|
callback.onFailure(IllegalArgumentException("url is null"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## FileService downloadFile $unwrappedUrl")
|
Timber.v("## FileService downloadFile $url")
|
||||||
|
|
||||||
synchronized(ongoing) {
|
synchronized(ongoing) {
|
||||||
val existing = ongoing[unwrappedUrl]
|
val existing = ongoing[url]
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
Timber.v("## FileService downloadFile is already downloading.. ")
|
Timber.v("## FileService downloadFile is already downloading.. ")
|
||||||
existing.add(callback)
|
existing.add(callback)
|
||||||
return NoOpCancellable
|
return NoOpCancellable
|
||||||
} else {
|
} else {
|
||||||
// mark as tracked
|
// mark as tracked
|
||||||
ongoing[unwrappedUrl] = ArrayList()
|
ongoing[url] = ArrayList()
|
||||||
// and proceed to download
|
// and proceed to download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
|
||||||
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
Try {
|
Try {
|
||||||
if (!downloadFolder.exists()) {
|
if (!decryptedFolder.exists()) {
|
||||||
downloadFolder.mkdirs()
|
decryptedFolder.mkdirs()
|
||||||
}
|
}
|
||||||
// ensure we use unique file name by using URL (mapped to suitable file name)
|
// 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
|
// 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)
|
// shared with will not function well (even if mime type is passed in the intent)
|
||||||
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType))
|
getFiles(url, fileName, mimeType, elementToDecrypt != null)
|
||||||
}.flatMap { destFile ->
|
}.flatMap { cachedFiles ->
|
||||||
if (!destFile.exists()) {
|
if (!cachedFiles.file.exists()) {
|
||||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
|
@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
|
||||||
|
|
||||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||||
|
|
||||||
if (elementToDecrypt != null) {
|
// Write the file to cache (encrypted version if the file is encrypted)
|
||||||
Timber.v("## FileService: decrypt file")
|
writeToFile(source.inputStream(), cachedFiles.file)
|
||||||
val decryptSuccess = destFile.outputStream().buffered().use {
|
response.close()
|
||||||
MXEncryptedAttachments.decryptAttachment(
|
|
||||||
source.inputStream(),
|
|
||||||
elementToDecrypt,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
response.close()
|
|
||||||
if (!decryptSuccess) {
|
|
||||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeToFile(source.inputStream(), destFile)
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## FileService: cache hit for $url")
|
Timber.v("## FileService: cache hit for $url")
|
||||||
}
|
}
|
||||||
|
|
||||||
Try.just(copyFile(destFile, downloadMode))
|
Try.just(cachedFiles)
|
||||||
}
|
}
|
||||||
}.fold({
|
}.flatMap { cachedFiles ->
|
||||||
callback.onFailure(it)
|
// Decrypt if necessary
|
||||||
// notify concurrent requests
|
if (cachedFiles.decryptedFile != null) {
|
||||||
val toNotify = synchronized(ongoing) {
|
if (!cachedFiles.decryptedFile.exists()) {
|
||||||
ongoing[unwrappedUrl]?.also {
|
Timber.v("## FileService: decrypt file")
|
||||||
ongoing.remove(unwrappedUrl)
|
// 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 ->
|
}.fold(
|
||||||
tryOrNull { otherCallbacks.onFailure(it) }
|
{ throwable ->
|
||||||
}
|
callback.onFailure(throwable)
|
||||||
}, { file ->
|
// notify concurrent requests
|
||||||
callback.onSuccess(file)
|
val toNotify = synchronized(ongoing) {
|
||||||
// notify concurrent requests
|
ongoing[url]?.also {
|
||||||
val toNotify = synchronized(ongoing) {
|
ongoing.remove(url)
|
||||||
ongoing[unwrappedUrl]?.also {
|
}
|
||||||
ongoing.remove(unwrappedUrl)
|
}
|
||||||
|
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()
|
}.toCancelable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) {
|
fun storeDataFor(mxcUrl: String,
|
||||||
val file = File(downloadFolder, fileForUrl(url, mimeType))
|
filename: String?,
|
||||||
val source = inputStream.source().buffer()
|
mimeType: String?,
|
||||||
file.sink().buffer().let { sink ->
|
originalFile: File,
|
||||||
source.use { input ->
|
encryptedFile: File?) {
|
||||||
sink.use { output ->
|
val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
|
||||||
output.writeAll(input)
|
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 {
|
override fun isFileInCache(mxcUrl: String?,
|
||||||
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
fileName: String,
|
||||||
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName()
|
mimeType: String?,
|
||||||
|
elementToDecrypt: ElementToDecrypt?): Boolean {
|
||||||
|
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean {
|
internal data class CachedFiles(
|
||||||
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists()
|
// 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 {
|
private fun getFiles(mxcUrl: String,
|
||||||
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE
|
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) {
|
val isDownloading = synchronized(ongoing) {
|
||||||
ongoing[mxcUrl] != null
|
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
|
* 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)
|
* (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?
|
// this string could be extracted no?
|
||||||
val authority = "${context.packageName}.mx-sdk.fileprovider"
|
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
|
if (!targetFile.exists()) return null
|
||||||
return FileProvider.getUriForFile(context, authority, targetFile)
|
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 {
|
override fun getCacheSize(): Int {
|
||||||
return downloadFolder.walkTopDown()
|
return downloadFolder.walkTopDown()
|
||||||
.onEnter {
|
.onEnter {
|
||||||
|
@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
|
||||||
override fun clearCache() {
|
override fun clearCache() {
|
||||||
downloadFolder.deleteRecursively()
|
downloadFolder.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearDecryptedCache() {
|
||||||
|
decryptedFolder.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ENCRYPTED_FILENAME = "encrypted.bin"
|
||||||
|
// The extension would be added from the mimetype
|
||||||
|
private const val DEFAULT_FILENAME = "file"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService
|
||||||
import org.matrix.android.sdk.api.session.group.GroupService
|
import org.matrix.android.sdk.api.session.group.GroupService
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
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.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.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
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 permalinkService: Lazy<PermalinkService>,
|
||||||
private val secureStorageService: Lazy<SecureStorageService>,
|
private val secureStorageService: Lazy<SecureStorageService>,
|
||||||
private val profileService: Lazy<ProfileService>,
|
private val profileService: Lazy<ProfileService>,
|
||||||
|
private val mediaService: Lazy<MediaService>,
|
||||||
private val widgetService: Lazy<WidgetService>,
|
private val widgetService: Lazy<WidgetService>,
|
||||||
private val syncThreadProvider: Provider<SyncThread>,
|
private val syncThreadProvider: Provider<SyncThread>,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
|
@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
|
|
||||||
override fun widgetService(): WidgetService = widgetService.get()
|
override fun widgetService(): WidgetService = widgetService.get()
|
||||||
|
|
||||||
|
override fun mediaService(): MediaService = mediaService.get()
|
||||||
|
|
||||||
override fun integrationManagerService() = integrationManagerService
|
override fun integrationManagerService() = integrationManagerService
|
||||||
|
|
||||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule
|
||||||
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
|
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
|
||||||
import org.matrix.android.sdk.internal.session.identity.IdentityModule
|
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.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.openid.OpenIdModule
|
||||||
import org.matrix.android.sdk.internal.session.profile.ProfileModule
|
import org.matrix.android.sdk.internal.session.profile.ProfileModule
|
||||||
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
||||||
|
@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
GroupModule::class,
|
GroupModule::class,
|
||||||
ContentModule::class,
|
ContentModule::class,
|
||||||
CacheModule::class,
|
CacheModule::class,
|
||||||
|
MediaModule::class,
|
||||||
CryptoModule::class,
|
CryptoModule::class,
|
||||||
PushersModule::class,
|
PushersModule::class,
|
||||||
OpenIdModule::class,
|
OpenIdModule::class,
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||||
import org.matrix.android.sdk.internal.di.Authenticated
|
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.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||||
|
@ -169,9 +170,9 @@ internal abstract class SessionModule {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@SessionDownloadsDirectory
|
@SessionDownloadsDirectory
|
||||||
fun providesCacheDir(@SessionId sessionId: String,
|
fun providesDownloadsCacheDir(@SessionId sessionId: String,
|
||||||
context: Context): File {
|
@CacheDirectory cacheFile: File): File {
|
||||||
return File(context.cacheDir, "downloads/$sessionId")
|
return File(cacheFile, "downloads/$sessionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -20,6 +20,9 @@ import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@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
|
@Json(name = "content_uri") val contentUri: String
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ internal object ThumbnailExtractor {
|
||||||
height = thumbnailHeight,
|
height = thumbnailHeight,
|
||||||
size = thumbnailSize.toLong(),
|
size = thumbnailSize.toLong(),
|
||||||
bytes = outputStream.toByteArray(),
|
bytes = outputStream.toByteArray(),
|
||||||
mimeType = "image/jpeg"
|
mimeType = MimeTypes.Jpeg
|
||||||
)
|
)
|
||||||
thumbnail.recycle()
|
thumbnail.recycle()
|
||||||
outputStream.reset()
|
outputStream.reset()
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
import org.matrix.android.sdk.api.session.room.model.message.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.attachments.MXEncryptedAttachments
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||||
|
@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
params.attachment.size
|
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)
|
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
||||||
.also { compressedFile ->
|
.also { compressedFile ->
|
||||||
// Get new Bitmap size
|
// Get new Bitmap size
|
||||||
|
@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val encryptedFile: File?
|
||||||
val contentUploadResponse = if (params.isEncrypted) {
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
Timber.v("## FileService: Encrypt file")
|
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) }
|
.also { filesToDelete.add(it) }
|
||||||
|
|
||||||
uploadedFileEncryptedFileInfo =
|
uploadedFileEncryptedFileInfo =
|
||||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
|
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
|
||||||
notifyTracker(params) {
|
notifyTracker(params) {
|
||||||
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
|
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
|
||||||
}
|
}
|
||||||
|
@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
Timber.v("## FileService: Uploading file")
|
Timber.v("## FileService: Uploading file")
|
||||||
|
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
|
.uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## FileService: Clear file")
|
Timber.v("## FileService: Clear file")
|
||||||
|
encryptedFile = null
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
|
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
|
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
fileService.storeDataFor(
|
||||||
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
mxcUrl = contentUploadResponse.contentUri,
|
||||||
}
|
filename = params.attachment.name,
|
||||||
|
mimeType = params.attachment.getSafeMimeType(),
|
||||||
|
originalFile = workingFile,
|
||||||
|
encryptedFile = encryptedFile
|
||||||
|
)
|
||||||
Timber.v("## FileService: cache storage updated")
|
Timber.v("## FileService: cache storage updated")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "## FileService: Failed to update file cache")
|
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 encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
|
||||||
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
"thumb_${params.attachment.name}",
|
"thumb_${params.attachment.name}",
|
||||||
"application/octet-stream",
|
MimeTypes.OctetStream,
|
||||||
thumbnailProgressListener)
|
thumbnailProgressListener)
|
||||||
UploadThumbnailResult(
|
UploadThumbnailResult(
|
||||||
contentUploadResponse.contentUri,
|
contentUploadResponse.contentUri,
|
||||||
|
|
|
@ -22,19 +22,12 @@ import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
|
||||||
internal interface CapabilitiesAPI {
|
internal interface CapabilitiesAPI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the homeserver capabilities
|
* Request the homeserver capabilities
|
||||||
*/
|
*/
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
||||||
fun getCapabilities(): Call<GetCapabilitiesResult>
|
fun getCapabilities(): Call<GetCapabilitiesResult>
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the upload capabilities
|
|
||||||
*/
|
|
||||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
|
||||||
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the versions
|
* Request the versions
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
|
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.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
|
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
|
||||||
|
@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
|
||||||
|
|
||||||
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
private val capabilitiesAPI: CapabilitiesAPI,
|
private val capabilitiesAPI: CapabilitiesAPI,
|
||||||
|
private val mediaAPI: MediaAPI,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
private val getWellknownTask: GetWellknownTask,
|
private val getWellknownTask: GetWellknownTask,
|
||||||
|
@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
val uploadCapabilities = runCatching {
|
val mediaConfig = runCatching {
|
||||||
executeRequest<GetUploadCapabilitiesResult>(eventBus) {
|
executeRequest<GetMediaConfigResult>(eventBus) {
|
||||||
apiCall = capabilitiesAPI.getUploadCapabilities()
|
apiCall = mediaAPI.getMediaConfig()
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
|
@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
|
insertInDb(capabilities, mediaConfig, versions, wellknownResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
|
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
|
||||||
getUploadCapabilitiesResult: GetUploadCapabilitiesResult?,
|
getMediaConfigResult: GetMediaConfigResult?,
|
||||||
getVersionResult: Versions?,
|
getVersionResult: Versions?,
|
||||||
getWellknownResult: WellknownResult?) {
|
getWellknownResult: WellknownResult?) {
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
|
@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getUploadCapabilitiesResult != null) {
|
if (getMediaConfigResult != null) {
|
||||||
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize
|
||||||
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface ClearPreviewUrlCacheTask : Task<Unit, Unit>
|
||||||
|
|
||||||
|
internal class DefaultClearPreviewUrlCacheTask @Inject constructor(
|
||||||
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
|
) : ClearPreviewUrlCacheTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: Unit) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
realm.where<PreviewUrlCacheEntity>()
|
||||||
|
.findAll()
|
||||||
|
.deleteAllFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.media.MediaService
|
||||||
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.util.getOrPut
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DefaultMediaService @Inject constructor(
|
||||||
|
private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask,
|
||||||
|
private val getPreviewUrlTask: GetPreviewUrlTask,
|
||||||
|
private val getRawPreviewUrlTask: GetRawPreviewUrlTask,
|
||||||
|
private val urlsExtractor: UrlsExtractor
|
||||||
|
) : MediaService {
|
||||||
|
// Cache of extracted URLs
|
||||||
|
private val extractedUrlsCache = LruCache<String, List<String>>(1_000)
|
||||||
|
|
||||||
|
override fun extractUrls(event: Event): List<String> {
|
||||||
|
return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}"
|
||||||
|
|
||||||
|
override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict {
|
||||||
|
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData {
|
||||||
|
return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clearCache() {
|
||||||
|
extractedUrlsCache.evictAll()
|
||||||
|
clearPreviewUrlCacheTask.execute(Unit)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@ -14,13 +14,13 @@
|
||||||
* limitations under the License.
|
* 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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@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.
|
* 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.
|
* If not listed or null, the size limit should be treated as unknown.
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||||
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface GetPreviewUrlTask : Task<GetPreviewUrlTask.Params, PreviewUrlData> {
|
||||||
|
data class Params(
|
||||||
|
val url: String,
|
||||||
|
val timestamp: Long?,
|
||||||
|
val cacheStrategy: CacheStrategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultGetPreviewUrlTask @Inject constructor(
|
||||||
|
private val mediaAPI: MediaAPI,
|
||||||
|
private val eventBus: EventBus,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
|
) : GetPreviewUrlTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
||||||
|
return when (params.cacheStrategy) {
|
||||||
|
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||||
|
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||||
|
params.url,
|
||||||
|
params.timestamp,
|
||||||
|
params.cacheStrategy.validityDurationInMillis,
|
||||||
|
params.cacheStrategy.strict
|
||||||
|
)
|
||||||
|
CacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||||
|
params.url,
|
||||||
|
params.timestamp,
|
||||||
|
Long.MAX_VALUE,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData {
|
||||||
|
return executeRequest<JsonDict>(eventBus) {
|
||||||
|
apiCall = mediaAPI.getPreviewUrlData(url, timestamp)
|
||||||
|
}
|
||||||
|
.toPreviewUrlData(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
|
||||||
|
return PreviewUrlData(
|
||||||
|
url = (get("og:url") as? String) ?: url,
|
||||||
|
siteName = get("og:site_name") as? String,
|
||||||
|
title = get("og:title") as? String,
|
||||||
|
description = get("og:description") as? String,
|
||||||
|
mxcUrl = get("og:image") as? String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData {
|
||||||
|
// Get data from cache
|
||||||
|
var dataFromCache: PreviewUrlData? = null
|
||||||
|
var isCacheValid = false
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
val entity = PreviewUrlCacheEntity.get(realm, url)
|
||||||
|
dataFromCache = entity?.toDomain()
|
||||||
|
isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalDataFromCache = dataFromCache
|
||||||
|
if (finalDataFromCache != null && isCacheValid) {
|
||||||
|
return finalDataFromCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cache or outdated cache
|
||||||
|
val data = try {
|
||||||
|
doRequest(url, timestamp)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
// In case of error, we can return value from cache even if outdated
|
||||||
|
return finalDataFromCache
|
||||||
|
?.takeIf { !strict }
|
||||||
|
?: throw throwable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store cache
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url)
|
||||||
|
previewUrlCacheEntity.urlFromServer = data.url
|
||||||
|
previewUrlCacheEntity.siteName = data.siteName
|
||||||
|
previewUrlCacheEntity.title = data.title
|
||||||
|
previewUrlCacheEntity.description = data.description
|
||||||
|
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
||||||
|
|
||||||
|
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface GetRawPreviewUrlTask : Task<GetRawPreviewUrlTask.Params, JsonDict> {
|
||||||
|
data class Params(
|
||||||
|
val url: String,
|
||||||
|
val timestamp: Long?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultGetRawPreviewUrlTask @Inject constructor(
|
||||||
|
private val mediaAPI: MediaAPI,
|
||||||
|
private val eventBus: EventBus
|
||||||
|
) : GetRawPreviewUrlTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict {
|
||||||
|
return executeRequest(eventBus) {
|
||||||
|
apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
internal interface MediaAPI {
|
||||||
|
/**
|
||||||
|
* Retrieve the configuration of the content repository
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
||||||
|
fun getMediaConfig(): Call<GetMediaConfigResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about a URL for the client. Typically this is called when a client
|
||||||
|
* sees a URL in a message and wants to render a preview for the user.
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-url
|
||||||
|
* @param url Required. The URL to get a preview of.
|
||||||
|
* @param ts The preferred point in time to return a preview for. The server may return a newer version
|
||||||
|
* if it does not have the requested version available.
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url")
|
||||||
|
fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call<JsonDict>
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.matrix.android.sdk.api.session.media.MediaService
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class MediaModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@SessionScope
|
||||||
|
fun providesMediaAPI(retrofit: Retrofit): MediaAPI {
|
||||||
|
return retrofit.create(MediaAPI::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindMediaService(service: DefaultMediaService): MediaService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreviewUrlCacheEntity -> PreviewUrlData
|
||||||
|
*/
|
||||||
|
internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
||||||
|
url = urlFromServer ?: url,
|
||||||
|
siteName = siteName,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
mxcUrl = mxcUrl
|
||||||
|
)
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.media
|
||||||
|
|
||||||
|
import android.util.Patterns
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class UrlsExtractor @Inject constructor() {
|
||||||
|
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
|
||||||
|
private val urlRegex = Patterns.WEB_URL.toRegex()
|
||||||
|
|
||||||
|
fun extract(event: Event): List<String> {
|
||||||
|
return event.takeIf { it.getClearType() == EventType.MESSAGE }
|
||||||
|
?.getClearContent()
|
||||||
|
?.toModel<MessageContent>()
|
||||||
|
?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_EMOTE }
|
||||||
|
?.body
|
||||||
|
?.let { urlRegex.findAll(it) }
|
||||||
|
?.map { it.value }
|
||||||
|
?.filter { it.startsWith("https://") || it.startsWith("http://") }
|
||||||
|
?.distinct()
|
||||||
|
?.toList()
|
||||||
|
.orEmpty()
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
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.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
|
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 {
|
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
|
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))
|
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
|
||||||
userStore.updateAvatar(userId, response.contentUri)
|
userStore.updateAvatar(userId, response.contentUri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.IdentityServiceError
|
||||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
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.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.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
||||||
|
@ -96,7 +97,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
fileUploader.uploadFromUri(
|
fileUploader.uploadFromUri(
|
||||||
uri = avatarUri,
|
uri = avatarUri,
|
||||||
filename = UUID.randomUUID().toString(),
|
filename = UUID.randomUUID().toString(),
|
||||||
mimeType = "image/jpeg")
|
mimeType = MimeTypes.Jpeg)
|
||||||
}
|
}
|
||||||
?.let { response ->
|
?.let { response ->
|
||||||
Event(
|
Event(
|
||||||
|
|
|
@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
val attachmentData = ContentAttachmentData(
|
val attachmentData = ContentAttachmentData(
|
||||||
size = messageContent.info!!.size,
|
size = messageContent.info!!.size,
|
||||||
mimeType = messageContent.info.mimeType!!,
|
mimeType = messageContent.info.mimeType!!,
|
||||||
name = messageContent.body,
|
name = messageContent.getFileName(),
|
||||||
queryUri = Uri.parse(messageContent.url),
|
queryUri = Uri.parse(messageContent.url),
|
||||||
type = ContentAttachmentData.Type.FILE
|
type = ContentAttachmentData.Type.FILE
|
||||||
)
|
)
|
||||||
|
@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
|
|
||||||
override fun cancelSend(eventId: String) {
|
override fun cancelSend(eventId: String) {
|
||||||
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
|
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
|
||||||
|
// This is maybe the current task, so cancel it too
|
||||||
|
eventSenderProcessor.cancel(eventId, roomId)
|
||||||
taskExecutor.executorScope.launch {
|
taskExecutor.executorScope.launch {
|
||||||
localEchoRepository.deleteFailedEcho(roomId, eventId)
|
localEchoRepository.deleteFailedEcho(roomId, eventId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||||
|
@ -106,17 +107,21 @@ internal class EventSenderProcessor @Inject constructor(
|
||||||
// non blocking add to queue
|
// non blocking add to queue
|
||||||
sendingQueue.add(task)
|
sendingQueue.add(task)
|
||||||
markAsManaged(task)
|
markAsManaged(task)
|
||||||
return object : Cancelable {
|
return task
|
||||||
override fun cancel() {
|
}
|
||||||
task.cancel()
|
|
||||||
}
|
fun cancel(eventId: String, roomId: String) {
|
||||||
}
|
(currentTask as? SendEventQueuedTask)
|
||||||
|
?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId }
|
||||||
|
?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var currentTask: QueuedTask? = null
|
||||||
|
|
||||||
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
|
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
|
||||||
|
|
||||||
private var networkAvailableLock = Object()
|
private var networkAvailableLock = Object()
|
||||||
|
@ -129,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor(
|
||||||
while (!isInterrupted) {
|
while (!isInterrupted) {
|
||||||
Timber.v("## SendThread wait for task to process")
|
Timber.v("## SendThread wait for task to process")
|
||||||
val task = sendingQueue.take()
|
val task = sendingQueue.take()
|
||||||
|
.also { currentTask = it }
|
||||||
Timber.v("## SendThread Found task to process $task")
|
Timber.v("## SendThread Found task to process $task")
|
||||||
|
|
||||||
if (task.isCancelled()) {
|
if (task.isCancelled()) {
|
||||||
|
@ -183,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor(
|
||||||
task.onTaskFailed()
|
task.onTaskFailed()
|
||||||
throw InterruptedException()
|
throw InterruptedException()
|
||||||
}
|
}
|
||||||
|
exception is CancellationException -> {
|
||||||
|
Timber.v("## SendThread task has been cancelled")
|
||||||
|
break@retryLoop
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
|
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
|
||||||
// this task is in error, check next one?
|
// this task is in error, check next one?
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||||
|
|
||||||
import android.content.Context
|
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.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
|
|
@ -16,14 +16,26 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
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
|
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 onTaskFailed()
|
||||||
|
|
||||||
abstract fun isCancelled() : Boolean
|
open fun isCancelled() = hasBeenCancelled
|
||||||
|
|
||||||
abstract fun cancel()
|
final override fun cancel() {
|
||||||
|
hasBeenCancelled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,20 +22,18 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||||
|
|
||||||
internal class RedactQueuedTask(
|
internal class RedactQueuedTask(
|
||||||
val toRedactEventId: String,
|
private val toRedactEventId: String,
|
||||||
val redactionLocalEchoId: String,
|
val redactionLocalEchoId: String,
|
||||||
val roomId: String,
|
private val roomId: String,
|
||||||
val reason: String?,
|
private val reason: String?,
|
||||||
val redactEventTask: RedactEventTask,
|
private val redactEventTask: RedactEventTask,
|
||||||
val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
val cancelSendTracker: CancelSendTracker
|
private val cancelSendTracker: CancelSendTracker
|
||||||
) : QueuedTask() {
|
) : QueuedTask() {
|
||||||
|
|
||||||
private var _isCancelled: Boolean = false
|
override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]"
|
||||||
|
|
||||||
override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]"
|
override suspend fun doExecute() {
|
||||||
|
|
||||||
override suspend fun execute() {
|
|
||||||
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
|
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +42,6 @@ internal class RedactQueuedTask(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCancelled(): Boolean {
|
override fun isCancelled(): Boolean {
|
||||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
|
return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
_isCancelled = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,9 @@ internal class SendEventQueuedTask(
|
||||||
val cancelSendTracker: CancelSendTracker
|
val cancelSendTracker: CancelSendTracker
|
||||||
) : QueuedTask() {
|
) : QueuedTask() {
|
||||||
|
|
||||||
private var _isCancelled: Boolean = false
|
override fun toString() = "[SendEventQueuedTask ${event.eventId}]"
|
||||||
|
|
||||||
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
|
override suspend fun doExecute() {
|
||||||
|
|
||||||
override suspend fun execute() {
|
|
||||||
sendEventTask.execute(SendEventTask.Params(event, encrypt))
|
sendEventTask.execute(SendEventTask.Params(event, encrypt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +54,6 @@ internal class SendEventQueuedTask(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCancelled(): Boolean {
|
override fun isCancelled(): Boolean {
|
||||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
|
return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
_isCancelled = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ 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.model.RoomJoinRulesContent
|
||||||
import org.matrix.android.sdk.api.session.room.state.StateService
|
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
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.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
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.session.room.alias.AddRoomAliasTask
|
||||||
|
@ -137,7 +138,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateAvatar(avatarUri: Uri, fileName: String) {
|
override suspend fun updateAvatar(avatarUri: Uri, fileName: String) {
|
||||||
val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg")
|
val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg)
|
||||||
sendStateEvent(
|
sendStateEvent(
|
||||||
eventType = EventType.STATE_ROOM_AVATAR,
|
eventType = EventType.STATE_ROOM_AVATAR,
|
||||||
body = mapOf("url" to response.contentUri),
|
body = mapOf("url" to response.contentUri),
|
||||||
|
|
|
@ -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.api.session.sync.SyncState
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
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.BackgroundDetectionObserver
|
||||||
import org.matrix.android.sdk.internal.util.Debouncer
|
import org.matrix.android.sdk.internal.util.Debouncer
|
||||||
import org.matrix.android.sdk.internal.util.createUIHandler
|
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
|
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
||||||
|
|
||||||
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
private val typingUsersTracker: DefaultTypingUsersTracker,
|
|
||||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||||
private val activeCallHandler: ActiveCallHandler
|
private val activeCallHandler: ActiveCallHandler
|
||||||
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||||
|
|
||||||
private var state: SyncState = SyncState.Idle
|
private var state: SyncState = SyncState.Idle
|
||||||
private var liveState = MutableLiveData<SyncState>(state)
|
private var liveState = MutableLiveData(state)
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
private val syncScope = CoroutineScope(SupervisorJob())
|
private val syncScope = CoroutineScope(SupervisorJob())
|
||||||
private val debouncer = Debouncer(createUIHandler())
|
private val debouncer = Debouncer(createUIHandler())
|
||||||
|
@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = newState
|
state = newState
|
||||||
debouncer.debounce("post_state", Runnable {
|
debouncer.debounce("post_state", {
|
||||||
liveState.value = newState
|
liveState.value = newState
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,9 @@ import java.io.InputStream
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun writeToFile(inputStream: InputStream, outputFile: File) {
|
fun writeToFile(inputStream: InputStream, outputFile: File) {
|
||||||
|
// Ensure the parent folder exists, else it will crash
|
||||||
|
outputFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
outputFile.outputStream().use {
|
outputFile.outputStream().use {
|
||||||
inputStream.copyTo(it)
|
inputStream.copyTo(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
|
@ -440,6 +440,10 @@ dependencies {
|
||||||
implementation 'com.google.zxing:core:3.3.3'
|
implementation 'com.google.zxing:core:3.3.3'
|
||||||
implementation 'me.dm7.barcodescanner:zxing:1.9.13'
|
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
|
// TESTS
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||||
|
|
|
@ -347,11 +347,6 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright 2017 Gabriel Ittner.
|
Copyright 2017 Gabriel Ittner.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<b>Android-multipicker-library</b>
|
|
||||||
<br/>
|
|
||||||
Copyright 2018 Kumar Bibek
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<b>htmlcompressor</b>
|
<b>htmlcompressor</b>
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -390,6 +385,11 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright 2018, Aleksandr Nikiforov
|
Copyright 2018, Aleksandr Nikiforov
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Emoji</b>
|
||||||
|
<br/>
|
||||||
|
Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<pre>
|
<pre>
|
||||||
Apache License
|
Apache License
|
||||||
|
|
|
@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.facebook.stetho.Stetho
|
import com.facebook.stetho.Stetho
|
||||||
import com.gabrielittner.threetenbp.LazyThreeTen
|
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.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.DaggerVectorComponent
|
import im.vector.app.core.di.DaggerVectorComponent
|
||||||
import im.vector.app.core.di.HasVectorInjector
|
import im.vector.app.core.di.HasVectorInjector
|
||||||
|
@ -184,6 +186,8 @@ class VectorApplication :
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
addAction(Intent.ACTION_SCREEN_ON)
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
EmojiManager.install(GoogleEmojiProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableStrictModeIfNeeded() {
|
private fun enableStrictModeIfNeeded() {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
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
|
// Use the file vector service, will avoid flickering and redownload after upload
|
||||||
fileService.downloadFile(
|
fileService.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
|
||||||
mimeType = data.mimeType,
|
|
||||||
id = data.eventId,
|
|
||||||
url = data.url,
|
|
||||||
fileName = data.filename,
|
fileName = data.filename,
|
||||||
|
mimeType = data.mimeType,
|
||||||
|
url = data.url,
|
||||||
elementToDecrypt = data.elementToDecrypt,
|
elementToDecrypt = data.elementToDecrypt,
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
|
|
|
@ -20,17 +20,11 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import im.vector.app.core.utils.getFileExtension
|
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 timber.log.Timber
|
||||||
import java.io.InputStream
|
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(
|
data class Resource(
|
||||||
var mContentStream: InputStream? = null,
|
var mContentStream: InputStream? = null,
|
||||||
var mMimeType: String? = null
|
var mMimeType: String? = null
|
||||||
|
@ -55,7 +49,7 @@ data class Resource(
|
||||||
* @return true if the opened resource is a jpeg one.
|
* @return true if the opened resource is a jpeg one.
|
||||||
*/
|
*/
|
||||||
fun isJpegResource(): Boolean {
|
fun isJpegResource(): Boolean {
|
||||||
return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType
|
return mMimeType.normalizeMimeType() == MimeTypes.Jpeg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.core.ui.views
|
package im.vector.app.core.ui.views
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,10 @@ import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
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 timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
@ -138,7 +142,7 @@ fun openFileSelection(activity: Activity,
|
||||||
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection)
|
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection)
|
||||||
|
|
||||||
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
|
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
fileIntent.type = "*/*"
|
fileIntent.type = MimeTypes.Any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activityResultLauncher
|
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
|
// 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
|
// 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.
|
// 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
|
var dummyUri: Uri? = null
|
||||||
try {
|
try {
|
||||||
dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
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())
|
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
val externalContentUri = when {
|
val externalContentUri = when {
|
||||||
mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = context.contentResolver.insert(externalContentUri, values)
|
val uri = context.contentResolver.insert(externalContentUri, values)
|
||||||
|
@ -365,7 +369,7 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
|
||||||
notificationUtils.buildDownloadFileNotification(
|
notificationUtils.buildDownloadFileNotification(
|
||||||
uri,
|
uri,
|
||||||
filename,
|
filename,
|
||||||
mediaMimeType ?: "application/octet-stream"
|
mediaMimeType ?: MimeTypes.OctetStream
|
||||||
).let { notification ->
|
).let { notification ->
|
||||||
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
|
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
|
||||||
}
|
}
|
||||||
|
@ -385,10 +389,10 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val dest = when {
|
val dest = when {
|
||||||
mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES
|
mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES
|
||||||
mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES
|
mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES
|
||||||
mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC
|
mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC
|
||||||
else -> Environment.DIRECTORY_DOWNLOADS
|
else -> Environment.DIRECTORY_DOWNLOADS
|
||||||
}
|
}
|
||||||
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
|
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
|
||||||
try {
|
try {
|
||||||
|
@ -405,7 +409,7 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
|
||||||
savedFile.name,
|
savedFile.name,
|
||||||
title,
|
title,
|
||||||
true,
|
true,
|
||||||
mediaMimeType ?: "application/octet-stream",
|
mediaMimeType ?: MimeTypes.OctetStream,
|
||||||
savedFile.absolutePath,
|
savedFile.absolutePath,
|
||||||
savedFile.length(),
|
savedFile.length(),
|
||||||
true)
|
true)
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
|
@ -23,6 +23,9 @@ import im.vector.lib.multipicker.entity.MultiPickerFileType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerVideoType
|
import im.vector.lib.multipicker.entity.MultiPickerVideoType
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
|
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 timber.log.Timber
|
||||||
|
|
||||||
fun MultiPickerContactType.toContactAttachment(): ContactAttachment {
|
fun MultiPickerContactType.toContactAttachment(): ContactAttachment {
|
||||||
|
@ -59,10 +62,10 @@ fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
|
|
||||||
private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type {
|
private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type {
|
||||||
return when {
|
return when {
|
||||||
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
|
mimeType?.isMimeTypeImage() == true -> ContentAttachmentData.Type.IMAGE
|
||||||
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
|
mimeType?.isMimeTypeVideo() == true -> ContentAttachmentData.Type.VIDEO
|
||||||
mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
|
mimeType?.isMimeTypeAudio() == true -> ContentAttachmentData.Type.AUDIO
|
||||||
else -> ContentAttachmentData.Type.FILE
|
else -> ContentAttachmentData.Type.FILE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,19 @@
|
||||||
package im.vector.app.features.attachments
|
package im.vector.app.features.attachments
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
|
|
||||||
|
private val listOfPreviewableMimeTypes = listOf(
|
||||||
|
MimeTypes.Jpeg,
|
||||||
|
MimeTypes.BadJpg,
|
||||||
|
MimeTypes.Png,
|
||||||
|
MimeTypes.Gif
|
||||||
|
)
|
||||||
|
|
||||||
fun ContentAttachmentData.isPreviewable(): Boolean {
|
fun ContentAttachmentData.isPreviewable(): Boolean {
|
||||||
// For now the preview only supports still image
|
// For now the preview only supports still image
|
||||||
return type == ContentAttachmentData.Type.IMAGE
|
return type == ContentAttachmentData.Type.IMAGE
|
||||||
&& listOf("image/jpeg", "image/png", "image/jpg").contains(getSafeMimeType() ?: "")
|
&& listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GroupedContentAttachmentData(
|
data class GroupedContentAttachmentData(
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
package im.vector.app.features.attachments.preview
|
package im.vector.app.features.attachments.preview
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All images are editable, expect Gif
|
* All images are editable, expect Gif
|
||||||
*/
|
*/
|
||||||
fun ContentAttachmentData.isEditable(): Boolean {
|
fun ContentAttachmentData.isEditable(): Boolean {
|
||||||
return type == ContentAttachmentData.Type.IMAGE
|
return type == ContentAttachmentData.Type.IMAGE
|
||||||
&& getSafeMimeType()?.startsWith("image/") == true
|
&& getSafeMimeType()?.isMimeTypeImage() == true
|
||||||
&& getSafeMimeType() != "image/gif"
|
&& getSafeMimeType() != MimeTypes.Gif
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,12 +71,18 @@ class HomeActivityViewModel @AssistedInject constructor(
|
||||||
private var onceTrusted = false
|
private var onceTrusted = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
cleanupFiles()
|
||||||
observeInitialSync()
|
observeInitialSync()
|
||||||
mayBeInitializeCrossSigning()
|
mayBeInitializeCrossSigning()
|
||||||
checkSessionPushIsOn()
|
checkSessionPushIsOn()
|
||||||
observeCrossSigningReset()
|
observeCrossSigningReset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanupFiles() {
|
||||||
|
// Mitigation: delete all cached decrypted files each time the application is started.
|
||||||
|
activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeCrossSigningReset() {
|
private fun observeCrossSigningReset() {
|
||||||
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return
|
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||||
|
|
||||||
|
|
|
@ -98,4 +98,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
||||||
object QuickActionSetTopic : RoomDetailAction()
|
object QuickActionSetTopic : RoomDetailAction()
|
||||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
||||||
|
|
||||||
|
// Preview URL
|
||||||
|
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.OnModelBuildFinishedListener
|
import com.airbnb.epoxy.OnModelBuildFinishedListener
|
||||||
import com.airbnb.epoxy.addGlidePreloader
|
import com.airbnb.epoxy.addGlidePreloader
|
||||||
|
@ -69,6 +68,7 @@ import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
|
import com.vanniktech.emoji.EmojiPopup
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||||
|
@ -140,6 +140,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
|
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
|
@ -165,7 +166,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
import kotlinx.android.synthetic.main.composer_layout.view.*
|
||||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
|
@ -174,7 +175,6 @@ import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
@ -185,7 +185,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
@ -194,7 +193,6 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -289,8 +287,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||||
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
||||||
|
|
||||||
@BindView(R.id.composerLayout)
|
|
||||||
lateinit var composerLayout: TextComposerView
|
|
||||||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||||
|
|
||||||
private var lockSendButton = false
|
private var lockSendButton = false
|
||||||
|
@ -311,6 +307,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
setupActiveCallView()
|
setupActiveCallView()
|
||||||
setupJumpToBottomView()
|
setupJumpToBottomView()
|
||||||
setupConfBannerView()
|
setupConfBannerView()
|
||||||
|
setupEmojiPopup()
|
||||||
|
|
||||||
roomToolbarContentView.debouncedClicks {
|
roomToolbarContentView.debouncedClicks {
|
||||||
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
||||||
|
@ -478,6 +475,20 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupEmojiPopup() {
|
||||||
|
val emojiPopup = EmojiPopup
|
||||||
|
.Builder
|
||||||
|
.fromRootView(rootConstraintLayout)
|
||||||
|
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
|
||||||
|
.setOnEmojiPopupShownListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_keyboard) }
|
||||||
|
.setOnEmojiPopupDismissListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_insert_emoji) }
|
||||||
|
.build(composerLayout.composerEditText)
|
||||||
|
|
||||||
|
composerLayout.composerEmojiButton.debouncedClicks {
|
||||||
|
emojiPopup.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
||||||
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
||||||
}
|
}
|
||||||
|
@ -1090,18 +1101,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val writingFileActivityResultLauncher = registerForPermissionsResult { allGranted ->
|
|
||||||
if (allGranted) {
|
|
||||||
val pendingUri = roomDetailViewModel.pendingUri
|
|
||||||
if (pendingUri != null) {
|
|
||||||
roomDetailViewModel.pendingUri = null
|
|
||||||
sendUri(pendingUri)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleanUpAfterPermissionNotGranted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
val composerEditText = composerLayout.composerEditText
|
val composerEditText = composerLayout.composerEditText
|
||||||
autoCompleter.setup(composerEditText)
|
autoCompleter.setup(composerEditText)
|
||||||
|
@ -1147,14 +1146,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
// We need WRITE_EXTERNAL permission
|
return sendUri(contentUri)
|
||||||
return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, requireActivity(), writingFileActivityResultLauncher)) {
|
|
||||||
sendUri(contentUri)
|
|
||||||
} else {
|
|
||||||
roomDetailViewModel.pendingUri = contentUri
|
|
||||||
// Always intercept when we request some permission
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1185,11 +1177,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendUri(uri: Uri): Boolean {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
roomDetailViewModel.preventAttachmentPreview = true
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent)
|
val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent)
|
||||||
if (!isHandled) {
|
if (!isHandled) {
|
||||||
roomDetailViewModel.preventAttachmentPreview = false
|
|
||||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return isHandled
|
return isHandled
|
||||||
|
@ -1211,9 +1201,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
|
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
|
||||||
timelineEventController.update(state)
|
timelineEventController.update(state)
|
||||||
inviteView.visibility = View.GONE
|
inviteView.visibility = View.GONE
|
||||||
val uid = session.myUserId
|
|
||||||
val meMember = state.myRoomMember()
|
|
||||||
avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView)
|
|
||||||
if (state.tombstoneEvent == null) {
|
if (state.tombstoneEvent == null) {
|
||||||
if (state.canSendMessage) {
|
if (state.canSendMessage) {
|
||||||
composerLayout.visibility = View.VISIBLE
|
composerLayout.visibility = View.VISIBLE
|
||||||
|
@ -1554,7 +1541,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private fun cleanUpAfterPermissionNotGranted() {
|
private fun cleanUpAfterPermissionNotGranted() {
|
||||||
// Reset all pending data
|
// Reset all pending data
|
||||||
roomDetailViewModel.pendingAction = null
|
roomDetailViewModel.pendingAction = null
|
||||||
roomDetailViewModel.pendingUri = null
|
|
||||||
attachmentsHelper.pendingType = null
|
attachmentsHelper.pendingType = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1630,6 +1616,10 @@ class RoomDetailFragment @Inject constructor(
|
||||||
roomDetailViewModel.handle(itemAction)
|
roomDetailViewModel.handle(itemAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPreviewUrlRetriever(): PreviewUrlRetriever {
|
||||||
|
return roomDetailViewModel.previewUrlRetriever
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRoomCreateLinkClicked(url: String) {
|
override fun onRoomCreateLinkClicked(url: String) {
|
||||||
permalinkHandler
|
permalinkHandler
|
||||||
.launch(requireContext(), url, object : NavigationInterceptor {
|
.launch(requireContext(), url, object : NavigationInterceptor {
|
||||||
|
@ -1652,17 +1642,20 @@ class RoomDetailFragment @Inject constructor(
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreviewUrlClicked(url: String) {
|
||||||
|
onUrlClicked(url, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreviewUrlCloseClicked(eventId: String, url: String) {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
if (action.messageContent is MessageTextContent) {
|
if (action.messageContent is MessageTextContent) {
|
||||||
shareText(requireContext(), action.messageContent.body)
|
shareText(requireContext(), action.messageContent.body)
|
||||||
} else if (action.messageContent is MessageWithAttachmentContent) {
|
} else if (action.messageContent is MessageWithAttachmentContent) {
|
||||||
session.fileService().downloadFile(
|
session.fileService().downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
messageContent = action.messageContent,
|
||||||
id = action.eventId,
|
|
||||||
fileName = action.messageContent.body,
|
|
||||||
mimeType = action.messageContent.mimeType,
|
|
||||||
url = action.messageContent.getFileUrl(),
|
|
||||||
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
|
@ -1692,12 +1685,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.fileService().downloadFile(
|
session.fileService().downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
messageContent = action.messageContent,
|
||||||
id = action.eventId,
|
|
||||||
fileName = action.messageContent.body,
|
|
||||||
mimeType = action.messageContent.mimeType,
|
|
||||||
url = action.messageContent.getFileUrl(),
|
|
||||||
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
|
@ -1959,24 +1947,18 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// AttachmentsHelper.Callback
|
// AttachmentsHelper.Callback
|
||||||
|
|
||||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
if (roomDetailViewModel.preventAttachmentPreview) {
|
val grouped = attachments.toGroupedContentAttachmentData()
|
||||||
roomDetailViewModel.preventAttachmentPreview = false
|
if (grouped.notPreviewables.isNotEmpty()) {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments, false))
|
// Send the not previewable attachments right now (?)
|
||||||
} else {
|
roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
|
||||||
val grouped = attachments.toGroupedContentAttachmentData()
|
}
|
||||||
if (grouped.notPreviewables.isNotEmpty()) {
|
if (grouped.previewables.isNotEmpty()) {
|
||||||
// Send the not previewable attachments right now (?)
|
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
|
contentAttachmentActivityResultLauncher.launch(intent)
|
||||||
}
|
|
||||||
if (grouped.previewables.isNotEmpty()) {
|
|
||||||
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
|
|
||||||
contentAttachmentActivityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachmentsProcessFailed() {
|
override fun onAttachmentsProcessFailed() {
|
||||||
roomDetailViewModel.preventAttachmentPreview = false
|
|
||||||
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.home.room.typing.TypingHelper
|
import im.vector.app.features.home.room.typing.TypingHelper
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
|
@ -69,7 +70,6 @@ import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||||
|
@ -80,7 +80,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
||||||
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.session.room.model.message.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
|
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||||
|
@ -92,7 +91,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
|
@ -128,15 +126,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||||
val timeline = room.createTimeline(eventId, timelineSettings)
|
val timeline = room.createTimeline(eventId, timelineSettings)
|
||||||
|
|
||||||
|
// Same lifecycle than the ViewModel (survive to screen rotation)
|
||||||
|
val previewUrlRetriever = PreviewUrlRetriever(session)
|
||||||
|
|
||||||
// Slot to keep a pending action during permission request
|
// Slot to keep a pending action during permission request
|
||||||
var pendingAction: RoomDetailAction? = null
|
var pendingAction: RoomDetailAction? = null
|
||||||
|
|
||||||
// Slot to keep a pending uri during permission request
|
|
||||||
var pendingUri: Uri? = null
|
|
||||||
|
|
||||||
// Slot to store if we want to prevent preview of attachment
|
|
||||||
var preventAttachmentPreview = false
|
|
||||||
|
|
||||||
private var trackUnreadMessages = AtomicBoolean(false)
|
private var trackUnreadMessages = AtomicBoolean(false)
|
||||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||||
|
|
||||||
|
@ -286,9 +281,14 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) {
|
||||||
|
previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
|
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -1021,10 +1021,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
|
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
|
||||||
val mxcUrl = action.messageFileContent.getFileUrl()
|
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||||
val isLocalSendingFile = action.senderId == session.myUserId
|
val isLocalSendingFile = action.senderId == session.myUserId
|
||||||
&& mxcUrl?.startsWith("content://") ?: false
|
&& mxcUrl.startsWith("content://")
|
||||||
val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false
|
val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
|
||||||
if (isLocalSendingFile) {
|
if (isLocalSendingFile) {
|
||||||
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
||||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||||
|
@ -1035,7 +1035,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
} else if (isDownloaded) {
|
} else if (isDownloaded) {
|
||||||
// we can open it
|
// we can open it
|
||||||
session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri ->
|
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
||||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||||
action.messageFileContent.mimeType,
|
action.messageFileContent.mimeType,
|
||||||
uri,
|
uri,
|
||||||
|
@ -1044,12 +1044,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
session.fileService().downloadFile(
|
session.fileService().downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
messageContent = action.messageFileContent,
|
||||||
id = action.eventId,
|
|
||||||
fileName = action.messageFileContent.getFileName(),
|
|
||||||
mimeType = action.messageFileContent.mimeType,
|
|
||||||
url = mxcUrl,
|
|
||||||
elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
|
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
|
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
|
||||||
|
@ -1361,6 +1356,17 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
timelineEvents.accept(snapshot)
|
timelineEvents.accept(snapshot)
|
||||||
|
|
||||||
|
// PreviewUrl
|
||||||
|
if (vectorPreferences.showUrlPreviews()) {
|
||||||
|
withState { state ->
|
||||||
|
snapshot
|
||||||
|
.takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false }
|
||||||
|
?.forEach {
|
||||||
|
previewUrlRetriever.getPreviewUrl(it.root, viewModelScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimelineFailure(throwable: Throwable) {
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
|
|
|
@ -24,16 +24,16 @@ import android.text.Editable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
|
import com.vanniktech.emoji.EmojiEditText
|
||||||
import im.vector.app.core.extensions.ooi
|
import im.vector.app.core.extensions.ooi
|
||||||
import im.vector.app.core.platform.SimpleTextWatcher
|
import im.vector.app.core.platform.SimpleTextWatcher
|
||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
||||||
: AppCompatEditText(context, attrs, defStyleAttr) {
|
: EmojiEditText(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||||
|
|
|
@ -36,7 +36,7 @@ import androidx.transition.TransitionSet
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
import kotlinx.android.synthetic.main.composer_layout.view.*
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,8 +72,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
@BindView(R.id.composerEditText)
|
@BindView(R.id.composerEditText)
|
||||||
lateinit var composerEditText: ComposerEditText
|
lateinit var composerEditText: ComposerEditText
|
||||||
|
|
||||||
@BindView(R.id.composer_avatar_view)
|
@BindView(R.id.composer_emoji)
|
||||||
lateinit var composerAvatarImageView: ImageView
|
lateinit var composerEmojiButton: ImageButton
|
||||||
|
|
||||||
@BindView(R.id.composer_shield)
|
@BindView(R.id.composer_shield)
|
||||||
lateinit var composerShieldImageView: ImageView
|
lateinit var composerShieldImageView: ImageView
|
||||||
|
@ -86,7 +86,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
get() = composerEditText.text
|
get() = composerEditText.text
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflate(context, R.layout.merge_composer_layout, this)
|
inflate(context, R.layout.composer_layout, this)
|
||||||
ButterKnife.bind(this)
|
ButterKnife.bind(this)
|
||||||
collapse(false)
|
collapse(false)
|
||||||
composerEditText.callback = object : ComposerEditText.Callback {
|
composerEditText.callback = object : ComposerEditText.Callback {
|
||||||
|
@ -110,20 +110,20 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) {
|
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) {
|
||||||
// ignore we good
|
// ignore we good
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
|
currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) {
|
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) {
|
||||||
// ignore we good
|
// ignore we good
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
|
currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
@ -76,7 +77,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
private val backgroundHandler: Handler
|
private val backgroundHandler: Handler
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||||
|
|
||||||
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
|
interface Callback :
|
||||||
|
BaseCallback,
|
||||||
|
ReactionPillCallback,
|
||||||
|
AvatarCallback,
|
||||||
|
UrlClickCallback,
|
||||||
|
ReadReceiptsCallback,
|
||||||
|
PreviewUrlCallback {
|
||||||
fun onLoadMore(direction: Timeline.Direction)
|
fun onLoadMore(direction: Timeline.Direction)
|
||||||
fun onEventInvisible(event: TimelineEvent)
|
fun onEventInvisible(event: TimelineEvent)
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
|
@ -91,6 +98,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
|
|
||||||
// TODO move all callbacks to this?
|
// TODO move all callbacks to this?
|
||||||
fun onTimelineItemAction(itemAction: RoomDetailAction)
|
fun onTimelineItemAction(itemAction: RoomDetailAction)
|
||||||
|
|
||||||
|
fun getPreviewUrlRetriever(): PreviewUrlRetriever
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReactionPillCallback {
|
interface ReactionPillCallback {
|
||||||
|
@ -118,6 +127,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
fun onUrlLongClicked(url: String): Boolean
|
fun onUrlLongClicked(url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PreviewUrlCallback {
|
||||||
|
fun onPreviewUrlClicked(url: String)
|
||||||
|
fun onPreviewUrlCloseClicked(eventId: String, url: String)
|
||||||
|
}
|
||||||
|
|
||||||
// Map eventId to adapter position
|
// Map eventId to adapter position
|
||||||
private val adapterPositionMapping = HashMap<String, Int>()
|
private val adapterPositionMapping = HashMap<String, Int>()
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
|
|
|
@ -82,10 +82,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||||
when (cryptoError) {
|
when (cryptoError) {
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||||
span {
|
span {
|
||||||
apply {
|
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
||||||
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
image(it, "baseline")
|
||||||
image(it, "baseline")
|
+" "
|
||||||
}
|
|
||||||
}
|
}
|
||||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
|
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
|
||||||
textStyle = "italic"
|
textStyle = "italic"
|
||||||
|
@ -95,10 +94,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
span {
|
span {
|
||||||
apply {
|
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
||||||
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
image(it, "baseline")
|
||||||
image(it, "baseline")
|
+" "
|
||||||
}
|
|
||||||
}
|
}
|
||||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
|
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
|
||||||
textStyle = "italic"
|
textStyle = "italic"
|
||||||
|
|
|
@ -84,9 +84,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
|
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
|
||||||
|
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.session.room.model.message.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -144,16 +146,16 @@ class MessageItemFactory @Inject constructor(
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +166,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||||
return when (messageContent.optionType) {
|
return when (messageContent.optionType) {
|
||||||
OPTION_TYPE_POLL -> {
|
OPTION_TYPE_POLL -> {
|
||||||
MessagePollItem_()
|
MessagePollItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
|
@ -204,7 +206,12 @@ class MessageItemFactory @Inject constructor(
|
||||||
return MessageFileItem_()
|
return MessageFileItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.izLocalFile(fileUrl.isLocalFile())
|
.izLocalFile(fileUrl.isLocalFile())
|
||||||
.izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType))
|
.izDownloaded(session.fileService().isFileInCache(
|
||||||
|
fileUrl,
|
||||||
|
messageContent.getFileName(),
|
||||||
|
messageContent.mimeType,
|
||||||
|
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||||
|
)
|
||||||
.mxcUrl(fileUrl)
|
.mxcUrl(fileUrl)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||||
|
@ -264,7 +271,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.izLocalFile(messageContent.getFileUrl().isLocalFile())
|
.izLocalFile(messageContent.getFileUrl().isLocalFile())
|
||||||
.izDownloaded(session.fileService().isFileInCache(mxcUrl, messageContent.mimeType))
|
.izDownloaded(session.fileService().isFileInCache(messageContent))
|
||||||
.mxcUrl(mxcUrl)
|
.mxcUrl(mxcUrl)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||||
|
@ -305,7 +312,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.imageContentRenderer(imageContentRenderer)
|
.imageContentRenderer(imageContentRenderer)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.playable(messageContent.info?.mimeType == "image/gif")
|
.playable(messageContent.info?.mimeType == MimeTypes.Gif)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.mediaData(data)
|
.mediaData(data)
|
||||||
.apply {
|
.apply {
|
||||||
|
@ -371,7 +378,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
val codeVisitor = CodeVisitor()
|
val codeVisitor = CodeVisitor()
|
||||||
codeVisitor.visit(localFormattedBody)
|
codeVisitor.visit(localFormattedBody)
|
||||||
when (codeVisitor.codeKind) {
|
when (codeVisitor.codeKind) {
|
||||||
CodeVisitor.Kind.BLOCK -> {
|
CodeVisitor.Kind.BLOCK -> {
|
||||||
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
|
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
|
||||||
if (codeFormattedBlock == null) {
|
if (codeFormattedBlock == null) {
|
||||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
|
@ -387,7 +394,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CodeVisitor.Kind.NONE -> {
|
CodeVisitor.Kind.NONE -> {
|
||||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,6 +431,9 @@ class MessageItemFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
||||||
.searchForPills(isFormatted)
|
.searchForPills(isFormatted)
|
||||||
|
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
|
||||||
|
.imageContentRenderer(imageContentRenderer)
|
||||||
|
.previewUrlCallback(callback)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
|
@ -529,6 +539,9 @@ class MessageItemFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
|
||||||
|
.imageContentRenderer(imageContentRenderer)
|
||||||
|
.previewUrlCallback(callback)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.movementMethod(createLinkMovementMethod(callback))
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,12 @@ import androidx.core.widget.TextViewCompat
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
||||||
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
|
@ -37,10 +42,27 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var useBigFont: Boolean = false
|
var useBigFont: Boolean = false
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var previewUrlRetriever: PreviewUrlRetriever? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var imageContentRenderer: ImageContentRenderer? = null
|
||||||
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var movementMethod: MovementMethod? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
|
private val previewUrlViewUpdater = PreviewUrlViewUpdater()
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
|
// Preview URL
|
||||||
|
previewUrlViewUpdater.previewUrlView = holder.previewUrlView
|
||||||
|
previewUrlViewUpdater.imageContentRenderer = imageContentRenderer
|
||||||
|
previewUrlRetriever?.addListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||||
|
holder.previewUrlView.delegate = previewUrlCallback
|
||||||
|
|
||||||
if (useBigFont) {
|
if (useBigFont) {
|
||||||
holder.messageView.textSize = 44F
|
holder.messageView.textSize = 44F
|
||||||
} else {
|
} else {
|
||||||
|
@ -65,12 +87,29 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
holder.messageView.setTextFuture(textFuture)
|
holder.messageView.setTextFuture(textFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun unbind(holder: Holder) {
|
||||||
|
super.unbind(holder)
|
||||||
|
previewUrlViewUpdater.previewUrlView = null
|
||||||
|
previewUrlViewUpdater.imageContentRenderer = null
|
||||||
|
previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||||
|
val previewUrlView by bind<PreviewUrlView>(R.id.messageUrlPreview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener {
|
||||||
|
var previewUrlView: PreviewUrlView? = null
|
||||||
|
var imageContentRenderer: ImageContentRenderer? = null
|
||||||
|
|
||||||
|
override fun onStateUpdated(state: PreviewUrlUiState) {
|
||||||
|
val safeImageContentRenderer = imageContentRenderer ?: return
|
||||||
|
previewUrlView?.render(state, safeImageContentRenderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
companion object {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentTextStub
|
private const val STUB_ID = R.id.messageContentTextStub
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* 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.features.home.room.detail.timeline.url
|
||||||
|
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.cache.CacheStrategy
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
|
||||||
|
class PreviewUrlRetriever(session: Session) {
|
||||||
|
private val mediaService = session.mediaService()
|
||||||
|
|
||||||
|
private val data = mutableMapOf<String, PreviewUrlUiState>()
|
||||||
|
private val listeners = mutableMapOf<String, MutableSet<PreviewUrlRetrieverListener>>()
|
||||||
|
|
||||||
|
// In memory list
|
||||||
|
private val blockedUrl = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) {
|
||||||
|
val eventId = event.eventId ?: return
|
||||||
|
|
||||||
|
synchronized(data) {
|
||||||
|
if (data[eventId] == null) {
|
||||||
|
// Keep only the first URL for the moment
|
||||||
|
val url = mediaService.extractUrls(event)
|
||||||
|
.firstOrNull()
|
||||||
|
?.takeIf { it !in blockedUrl }
|
||||||
|
if (url == null) {
|
||||||
|
updateState(eventId, PreviewUrlUiState.NoUrl)
|
||||||
|
} else {
|
||||||
|
updateState(eventId, PreviewUrlUiState.Loading)
|
||||||
|
}
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
// Already handled
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}?.let { urlToRetrieve ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
mediaService.getPreviewUrl(
|
||||||
|
url = urlToRetrieve,
|
||||||
|
timestamp = null,
|
||||||
|
cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false)
|
||||||
|
)
|
||||||
|
}.fold(
|
||||||
|
{
|
||||||
|
synchronized(data) {
|
||||||
|
// Blocked after the request has been sent?
|
||||||
|
if (urlToRetrieve in blockedUrl) {
|
||||||
|
updateState(eventId, PreviewUrlUiState.NoUrl)
|
||||||
|
} else {
|
||||||
|
updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
synchronized(data) {
|
||||||
|
updateState(eventId, PreviewUrlUiState.Error(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doNotShowPreviewUrlFor(eventId: String, url: String) {
|
||||||
|
blockedUrl.add(url)
|
||||||
|
|
||||||
|
// Notify the listener
|
||||||
|
synchronized(data) {
|
||||||
|
data[eventId]
|
||||||
|
?.takeIf { it is PreviewUrlUiState.Data && it.url == url }
|
||||||
|
?.let {
|
||||||
|
updateState(eventId, PreviewUrlUiState.NoUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(eventId: String, state: PreviewUrlUiState) {
|
||||||
|
data[eventId] = state
|
||||||
|
// Notify the listener
|
||||||
|
listeners[eventId].orEmpty().forEach {
|
||||||
|
it.onStateUpdated(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the Epoxy item during binding
|
||||||
|
fun addListener(key: String, listener: PreviewUrlRetrieverListener) {
|
||||||
|
listeners.getOrPut(key) { mutableSetOf() }.add(listener)
|
||||||
|
|
||||||
|
// Give the current state if any
|
||||||
|
synchronized(data) {
|
||||||
|
listener.onStateUpdated(data[key] ?: PreviewUrlUiState.Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the Epoxy item during unbinding
|
||||||
|
fun removeListener(key: String, listener: PreviewUrlRetrieverListener) {
|
||||||
|
listeners[key]?.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreviewUrlRetrieverListener {
|
||||||
|
fun onStateUpdated(state: PreviewUrlUiState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// One week in millis
|
||||||
|
private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.features.home.room.detail.timeline.url
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state representing a preview url UI state for an Event
|
||||||
|
*/
|
||||||
|
sealed class PreviewUrlUiState {
|
||||||
|
// No info
|
||||||
|
object Unknown : PreviewUrlUiState()
|
||||||
|
|
||||||
|
// The event does not contain any URLs
|
||||||
|
object NoUrl : PreviewUrlUiState()
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
object Loading : PreviewUrlUiState()
|
||||||
|
|
||||||
|
// Error
|
||||||
|
data class Error(val throwable: Throwable) : PreviewUrlUiState()
|
||||||
|
|
||||||
|
// PreviewUrl data
|
||||||
|
data class Data(val eventId: String,
|
||||||
|
val url: String,
|
||||||
|
val previewUrlData: PreviewUrlData) : PreviewUrlUiState()
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* 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.features.home.room.detail.timeline.url
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A View to display a PreviewUrl and some other state
|
||||||
|
*/
|
||||||
|
class PreviewUrlView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
||||||
|
|
||||||
|
@BindView(R.id.url_preview_title)
|
||||||
|
lateinit var titleView: TextView
|
||||||
|
|
||||||
|
@BindView(R.id.url_preview_image)
|
||||||
|
lateinit var imageView: ImageView
|
||||||
|
|
||||||
|
@BindView(R.id.url_preview_description)
|
||||||
|
lateinit var descriptionView: TextView
|
||||||
|
|
||||||
|
@BindView(R.id.url_preview_site)
|
||||||
|
lateinit var siteView: TextView
|
||||||
|
|
||||||
|
@BindView(R.id.url_preview_close)
|
||||||
|
lateinit var closeView: View
|
||||||
|
|
||||||
|
var delegate: TimelineEventController.PreviewUrlCallback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods is responsible for rendering the view according to the newState
|
||||||
|
*
|
||||||
|
* @param newState the newState representing the view
|
||||||
|
*/
|
||||||
|
fun render(newState: PreviewUrlUiState,
|
||||||
|
imageContentRenderer: ImageContentRenderer,
|
||||||
|
force: Boolean = false) {
|
||||||
|
if (newState == state && !force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state = newState
|
||||||
|
|
||||||
|
hideAll()
|
||||||
|
when (newState) {
|
||||||
|
PreviewUrlUiState.Unknown,
|
||||||
|
PreviewUrlUiState.NoUrl -> renderHidden()
|
||||||
|
PreviewUrlUiState.Loading -> renderLoading()
|
||||||
|
is PreviewUrlUiState.Error -> renderHidden()
|
||||||
|
is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
when (val finalState = state) {
|
||||||
|
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCloseClick() {
|
||||||
|
when (val finalState = state) {
|
||||||
|
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
|
private fun setupView() {
|
||||||
|
inflate(context, R.layout.url_preview, this)
|
||||||
|
ButterKnife.bind(this)
|
||||||
|
|
||||||
|
setOnClickListener(this)
|
||||||
|
closeView.setOnClickListener { onCloseClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderHidden() {
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLoading() {
|
||||||
|
// Just hide for the moment
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) {
|
||||||
|
isVisible = true
|
||||||
|
titleView.setTextOrHide(previewUrlData.title)
|
||||||
|
imageView.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, imageView) }.orFalse()
|
||||||
|
descriptionView.setTextOrHide(previewUrlData.description)
|
||||||
|
siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide all views that are not visible in all state
|
||||||
|
*/
|
||||||
|
private fun hideAll() {
|
||||||
|
titleView.isVisible = false
|
||||||
|
imageView.isVisible = false
|
||||||
|
descriptionView.isVisible = false
|
||||||
|
siteView.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
|
@ -153,12 +153,10 @@ abstract class BaseAttachmentProvider<Type>(
|
||||||
} else {
|
} else {
|
||||||
target.onVideoFileLoading(info.uid)
|
target.onVideoFileLoading(info.uid)
|
||||||
fileService.downloadFile(
|
fileService.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
|
||||||
id = data.eventId,
|
|
||||||
mimeType = data.mimeType,
|
|
||||||
elementToDecrypt = data.elementToDecrypt,
|
|
||||||
fileName = data.filename,
|
fileName = data.filename,
|
||||||
|
mimeType = data.mimeType,
|
||||||
url = data.url,
|
url = data.url,
|
||||||
|
elementToDecrypt = data.elementToDecrypt,
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
target.onVideoFileReady(info.uid, data)
|
target.onVideoFileReady(info.uid, data)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
import org.matrix.android.sdk.api.session.file.FileService
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DataAttachmentRoomProvider(
|
class DataAttachmentRoomProvider(
|
||||||
|
@ -38,7 +39,7 @@ class DataAttachmentRoomProvider(
|
||||||
return getItem(position).let {
|
return getItem(position).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is ImageContentRenderer.Data -> {
|
is ImageContentRenderer.Data -> {
|
||||||
if (it.mimeType == "image/gif") {
|
if (it.mimeType == MimeTypes.Gif) {
|
||||||
AttachmentInfo.AnimatedImage(
|
AttachmentInfo.AnimatedImage(
|
||||||
uid = it.eventId,
|
uid = it.eventId,
|
||||||
url = it.url ?: "",
|
url = it.url ?: "",
|
||||||
|
@ -77,11 +78,9 @@ class DataAttachmentRoomProvider(
|
||||||
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
|
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
fileService.downloadFile(
|
fileService.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
|
||||||
id = item.eventId,
|
|
||||||
fileName = item.filename,
|
fileName = item.filename,
|
||||||
mimeType = item.mimeType,
|
mimeType = item.mimeType,
|
||||||
url = item.url ?: "",
|
url = item.url,
|
||||||
elementToDecrypt = item.elementToDecrypt,
|
elementToDecrypt = item.elementToDecrypt,
|
||||||
callback = object : MatrixCallback<File> {
|
callback = object : MatrixCallback<File> {
|
||||||
override fun onSuccess(data: File) {
|
override fun onSuccess(data: File) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
@ -83,6 +84,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
STICKER
|
STICKER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For url preview
|
||||||
|
*/
|
||||||
|
fun render(mxcUrl: String, imageView: ImageView): Boolean {
|
||||||
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
|
val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false
|
||||||
|
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.load(imageUrl)
|
||||||
|
.into(imageView)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For gallery
|
* For gallery
|
||||||
*/
|
*/
|
||||||
|
@ -129,6 +143,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
GlideApp
|
GlideApp
|
||||||
.with(contextView)
|
.with(contextView)
|
||||||
.load(data)
|
.load(data)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
// Clear image
|
// Clear image
|
||||||
val resolvedUrl = resolveUrl(data)
|
val resolvedUrl = resolveUrl(data)
|
||||||
|
@ -183,6 +198,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
GlideApp
|
GlideApp
|
||||||
.with(imageView)
|
.with(imageView)
|
||||||
.load(data)
|
.load(data)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
// Clear image
|
// Clear image
|
||||||
val resolvedUrl = resolveUrl(data)
|
val resolvedUrl = resolveUrl(data)
|
||||||
|
@ -214,20 +230,22 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
|
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
|
||||||
return if (data.elementToDecrypt != null) {
|
return if (data.elementToDecrypt != null) {
|
||||||
// Encrypted image
|
// Encrypted image
|
||||||
glideRequests.load(data)
|
glideRequests
|
||||||
|
.load(data)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
// Clear image
|
// Clear image
|
||||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
val resolvedUrl = when (mode) {
|
val resolvedUrl = when (mode) {
|
||||||
Mode.FULL_SIZE,
|
Mode.FULL_SIZE,
|
||||||
Mode.STICKER -> resolveUrl(data)
|
Mode.STICKER -> resolveUrl(data)
|
||||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
}
|
}
|
||||||
// Fallback to base url
|
// Fallback to base url
|
||||||
|
@ -295,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
|
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
|
||||||
finalWidth = finalHeight * width / height
|
finalWidth = finalHeight * width / height
|
||||||
}
|
}
|
||||||
Mode.STICKER -> {
|
Mode.STICKER -> {
|
||||||
// limit on width
|
// limit on width
|
||||||
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
|
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
|
||||||
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
|
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -56,7 +57,7 @@ class RoomEventsAttachmentProvider(
|
||||||
allowNonMxcUrls = it.root.sendState.isSending()
|
allowNonMxcUrls = it.root.sendState.isSending()
|
||||||
|
|
||||||
)
|
)
|
||||||
if (content.mimeType == "image/gif") {
|
if (content.mimeType == MimeTypes.Gif) {
|
||||||
AttachmentInfo.AnimatedImage(
|
AttachmentInfo.AnimatedImage(
|
||||||
uid = it.eventId,
|
uid = it.eventId,
|
||||||
url = content.url ?: "",
|
url = content.url ?: "",
|
||||||
|
@ -125,8 +126,6 @@ class RoomEventsAttachmentProvider(
|
||||||
as? MessageWithAttachmentContent
|
as? MessageWithAttachmentContent
|
||||||
?: return@let
|
?: return@let
|
||||||
fileService.downloadFile(
|
fileService.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
|
||||||
id = timelineEvent.eventId,
|
|
||||||
fileName = messageContent.body,
|
fileName = messageContent.body,
|
||||||
mimeType = messageContent.mimeType,
|
mimeType = messageContent.mimeType,
|
||||||
url = messageContent.getFileUrl(),
|
url = messageContent.getFileUrl(),
|
||||||
|
|
|
@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.utils.isLocalFile
|
import im.vector.app.core.utils.isLocalFile
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -76,8 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
|
|
||||||
activeSessionHolder.getActiveSession().fileService()
|
activeSessionHolder.getActiveSession().fileService()
|
||||||
.downloadFile(
|
.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
|
||||||
id = data.eventId,
|
|
||||||
fileName = data.filename,
|
fileName = data.filename,
|
||||||
mimeType = data.mimeType,
|
mimeType = data.mimeType,
|
||||||
url = data.url,
|
url = data.url,
|
||||||
|
@ -116,8 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
|
|
||||||
activeSessionHolder.getActiveSession().fileService()
|
activeSessionHolder.getActiveSession().fileService()
|
||||||
.downloadFile(
|
.downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
|
||||||
id = data.eventId,
|
|
||||||
fileName = data.filename,
|
fileName = data.filename,
|
||||||
mimeType = data.mimeType,
|
mimeType = data.mimeType,
|
||||||
url = data.url,
|
url = data.url,
|
||||||
|
|
|
@ -46,6 +46,7 @@ import okhttp3.Response
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -274,7 +275,7 @@ class BugReporter @Inject constructor(
|
||||||
|
|
||||||
// add the gzipped files
|
// add the gzipped files
|
||||||
for (file in gzippedFiles) {
|
for (file in gzippedFiles) {
|
||||||
builder.addFormDataPart("compressed-log", file.name, file.asRequestBody("application/octet-stream".toMediaTypeOrNull()))
|
builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
|
||||||
}
|
}
|
||||||
|
|
||||||
mBugReportFiles.addAll(gzippedFiles)
|
mBugReportFiles.addAll(gzippedFiles)
|
||||||
|
@ -295,7 +296,7 @@ class BugReporter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.addFormDataPart("file",
|
builder.addFormDataPart("file",
|
||||||
logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody("application/octet-stream".toMediaTypeOrNull()))
|
logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## sendBugReport() : fail to write screenshot$e")
|
Timber.e(e, "## sendBugReport() : fail to write screenshot$e")
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,7 @@ import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.file.FileService
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
|
||||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
import org.matrix.android.sdk.rx.unwrap
|
import org.matrix.android.sdk.rx.unwrap
|
||||||
|
@ -134,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
|
||||||
try {
|
try {
|
||||||
val file = awaitCallback<File> {
|
val file = awaitCallback<File> {
|
||||||
session.fileService().downloadFile(
|
session.fileService().downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
messageContent = action.uploadEvent.contentWithAttachmentContent,
|
||||||
id = action.uploadEvent.eventId,
|
|
||||||
fileName = action.uploadEvent.contentWithAttachmentContent.body,
|
|
||||||
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
|
|
||||||
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
|
|
||||||
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
|
|
||||||
callback = it
|
callback = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -155,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
|
||||||
try {
|
try {
|
||||||
val file = awaitCallback<File> {
|
val file = awaitCallback<File> {
|
||||||
session.fileService().downloadFile(
|
session.fileService().downloadFile(
|
||||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
messageContent = action.uploadEvent.contentWithAttachmentContent,
|
||||||
id = action.uploadEvent.eventId,
|
|
||||||
fileName = action.uploadEvent.contentWithAttachmentContent.body,
|
|
||||||
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
|
|
||||||
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
|
|
||||||
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
|
|
||||||
callback = it)
|
callback = it)
|
||||||
}
|
}
|
||||||
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
|
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
|
||||||
|
|
|
@ -783,6 +783,15 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||||
return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
|
return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if the user wants to see URL previews in the timeline
|
||||||
|
*
|
||||||
|
* @return true if the user wants to see URL previews in the timeline
|
||||||
|
*/
|
||||||
|
fun showUrlPreviews(): Boolean {
|
||||||
|
return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the analytics tracking.
|
* Enable or disable the analytics tracking.
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.widget.CheckedTextView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.SwitchPreference
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.restart
|
import im.vector.app.core.extensions.restart
|
||||||
import im.vector.app.core.preference.VectorListPreference
|
import im.vector.app.core.preference.VectorListPreference
|
||||||
|
@ -64,9 +63,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Url preview
|
// Url preview
|
||||||
|
/*
|
||||||
|
TODO Note: we keep the setting client side for now
|
||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
|
||||||
/*
|
|
||||||
TODO
|
|
||||||
it.isChecked = session.isURLPreviewEnabled
|
it.isChecked = session.isURLPreviewEnabled
|
||||||
|
|
||||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||||
|
@ -100,8 +99,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// update keep medias period
|
// update keep medias period
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let {
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let {
|
||||||
|
|
13
vector/src/main/res/drawable/bg_send.xml
Normal file
13
vector/src/main/res/drawable/bg_send.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:bottom="10dp"
|
||||||
|
android:left="10dp"
|
||||||
|
android:right="10dp"
|
||||||
|
android:top="10dp">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/riotx_accent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="?attr/selectableItemBackground" />
|
||||||
|
</layer-list>
|
|
@ -1,7 +1,21 @@
|
||||||
<vector android:autoMirrored="true" android:height="24dp"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="32dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="32dp"
|
||||||
<path android:fillColor="#ffffff"
|
android:viewportWidth="32"
|
||||||
android:pathData="M21,12C21,16.9706 16.9706,21 12,21C7.0294,21 3,16.9706 3,12C3,7.0294 7.0294,3 12,3C16.9706,3 21,7.0294 21,12ZM8,10C6.8954,10 6,10.8954 6,12C6,13.1046 6.8954,14 8,14H10V16C10,17.1046 10.8954,18 12,18C13.1046,18 14,17.1046 14,16V14H16C17.1046,14 18,13.1046 18,12C18,10.8954 17.1046,10 16,10H14V8C14,6.8954 13.1046,6 12,6C10.8954,6 10,6.8954 10,8V10H8Z"
|
android:viewportHeight="32">
|
||||||
android:strokeColor="#ffffff" android:strokeWidth="2"/>
|
<path
|
||||||
|
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
|
android:fillColor="#E3E8F0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M10.0009,16H22.0009"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#61708B"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16.0009,10V22"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#61708B"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
10
vector/src/main/res/drawable/ic_close_24dp.xml
Normal file
10
vector/src/main/res/drawable/ic_close_24dp.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="25"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M17.7929,5.2929C18.1834,4.9024 18.8166,4.9024 19.2071,5.2929C19.5976,5.6834 19.5976,6.3166 19.2071,6.7071L13.9142,12L19.2071,17.2929C19.5976,17.6834 19.5976,18.3166 19.2071,18.7071C18.8166,19.0976 18.1834,19.0976 17.7929,18.7071L12.5,13.4142L7.2071,18.7071C6.8166,19.0976 6.1834,19.0976 5.7929,18.7071C5.4024,18.3166 5.4024,17.6834 5.7929,17.2929L11.0858,12L5.7929,6.7071C5.4024,6.3166 5.4024,5.6834 5.7929,5.2929C6.1834,4.9024 6.8166,4.9024 7.2071,5.2929L12.5,10.5858L17.7929,5.2929Z" />
|
||||||
|
</vector>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue