mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-29 06:28:45 +03:00
Merge branch 'develop' into feature/bma/url_preview
This commit is contained in:
commit
91c86c1a45
66 changed files with 657 additions and 534 deletions
|
@ -5,12 +5,15 @@ 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)
|
- 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)
|
||||||
|
|
||||||
|
@ -19,15 +22,19 @@ Translations 🗣:
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
- RawCacheStrategy has been moved and renamed to CacheStrategy
|
- RawCacheStrategy has been moved and renamed to CacheStrategy
|
||||||
|
- FileService: remove useless FileService.DownloadMode
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
-
|
- Upgrade some dependencies and Kotlin version
|
||||||
|
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
|
||||||
|
- Upgrade Realm dependency to 10.1.2
|
||||||
|
|
||||||
Test:
|
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)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
|
@ -66,7 +66,6 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
ext.kotlin_version = '1.4.10'
|
ext.kotlin_version = '1.4.20'
|
||||||
ext.kotlin_coroutines_version = "1.3.9"
|
ext.kotlin_coroutines_version = "1.4.1"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||||
classpath 'com.google.gms:google-services:4.3.4'
|
classpath 'com.google.gms:google-services:4.3.4'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -36,9 +36,9 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix-sdk-android")
|
implementation project(":matrix-sdk-android")
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
|
||||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||||
|
|
||||||
// Paging
|
// Paging
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||||
|
|
||||||
|
|
|
@ -21,34 +21,36 @@ import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import io.reactivex.Completable
|
import io.reactivex.Completable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create {
|
fun <T> singleBuilder(builder: (MatrixCallback<T>) -> Cancelable): Single<T> = Single.create { emitter ->
|
||||||
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
|
val callback = object : MatrixCallback<T> {
|
||||||
override fun onSuccess(data: T) {
|
override fun onSuccess(data: T) {
|
||||||
it.onSuccess(data)
|
// Add `!!` to fix the warning:
|
||||||
|
// "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon"
|
||||||
|
emitter.onSuccess(data!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
it.tryOnError(failure)
|
emitter.tryOnError(failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val cancelable = builder(callback)
|
val cancelable = builder(callback)
|
||||||
it.setCancellable {
|
emitter.setCancellable {
|
||||||
cancelable.cancel()
|
cancelable.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
|
fun <T> completableBuilder(builder: (MatrixCallback<T>) -> Cancelable): Completable = Completable.create { emitter ->
|
||||||
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
|
val callback = object : MatrixCallback<T> {
|
||||||
override fun onSuccess(data: T) {
|
override fun onSuccess(data: T) {
|
||||||
it.onComplete()
|
emitter.onComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
it.tryOnError(failure)
|
emitter.tryOnError(failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val cancelable = builder(callback)
|
val cancelable = builder(callback)
|
||||||
it.setCancellable {
|
emitter.setCancellable {
|
||||||
cancelable.cancel()
|
cancelable.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,6 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
|
@ -146,7 +145,7 @@ dependencies {
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||||
import org.matrix.olm.OlmSAS
|
import org.matrix.olm.OlmSAS
|
||||||
import org.matrix.olm.OlmUtility
|
import org.matrix.olm.OlmUtility
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an ongoing short code interactive key verification between two devices.
|
* Represents an ongoing short code interactive key verification between two devices.
|
||||||
|
@ -344,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun hashUsingAgreedHashMethod(toHash: String): String? {
|
protected fun hashUsingAgreedHashMethod(toHash: String): String? {
|
||||||
if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) {
|
if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) {
|
||||||
val olmUtil = OlmUtility()
|
val olmUtil = OlmUtility()
|
||||||
val hashBytes = olmUtil.sha256(toHash)
|
val hashBytes = olmUtil.sha256(toHash)
|
||||||
olmUtil.releaseUtility()
|
olmUtil.releaseUtility()
|
||||||
|
@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun macUsingAgreedMethod(message: String, info: String): String? {
|
private fun macUsingAgreedMethod(message: String, info: String): String? {
|
||||||
if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
|
return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) {
|
||||||
return getSAS().calculateMacLongKdf(message, info)
|
SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
|
||||||
} else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
|
SAS_MAC_SHA256 -> getSAS().calculateMac(message, info)
|
||||||
return getSAS().calculateMac(message, info)
|
else -> null
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDecimalCodeRepresentation(): String {
|
override fun getDecimalCodeRepresentation(): String {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -174,14 +174,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 +191,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, "application/octet-stream", 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")
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.util
|
package org.matrix.android.sdk.internal.util
|
||||||
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute a Hash of a String, using md5 algorithm
|
* Compute a Hash of a String, using md5 algorithm
|
||||||
|
@ -26,7 +27,7 @@ fun String.md5() = try {
|
||||||
digest.update(toByteArray())
|
digest.update(toByteArray())
|
||||||
digest.digest()
|
digest.digest()
|
||||||
.joinToString("") { String.format("%02X", it) }
|
.joinToString("") { String.format("%02X", it) }
|
||||||
.toLowerCase()
|
.toLowerCase(Locale.ROOT)
|
||||||
} catch (exc: Exception) {
|
} catch (exc: Exception) {
|
||||||
// Should not happen, but just in case
|
// Should not happen, but just in case
|
||||||
hashCode().toString()
|
hashCode().toString()
|
||||||
|
|
|
@ -43,8 +43,8 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.fragment:fragment:1.3.0-beta01"
|
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.0'
|
implementation 'androidx.exifinterface:exifinterface:1.3.1'
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
|
@ -315,9 +315,8 @@ dependencies {
|
||||||
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06"
|
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06"
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.fragment:fragment:$fragment_version"
|
|
||||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
|
|
||||||
|
@ -362,11 +361,11 @@ dependencies {
|
||||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||||
|
|
||||||
// Pref
|
// Pref
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
implementation 'com.google.android.material:material:1.3.0-alpha04'
|
||||||
implementation 'me.gujun.android:span:1.7'
|
implementation 'me.gujun.android:span:1.7'
|
||||||
implementation "io.noties.markwon:core:$markwon_version"
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
implementation "io.noties.markwon:html:$markwon_version"
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
|
@ -374,7 +373,7 @@ dependencies {
|
||||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||||
implementation 'com.google.android:flexbox:1.1.1'
|
implementation 'com.google.android:flexbox:1.1.1'
|
||||||
implementation "androidx.autofill:autofill:$autofill_version"
|
implementation "androidx.autofill:autofill:$autofill_version"
|
||||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10'
|
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||||
|
|
||||||
// Custom Tab
|
// Custom Tab
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
@ -418,7 +417,7 @@ dependencies {
|
||||||
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
|
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
|
||||||
|
|
||||||
// gplay flavor only
|
// gplay flavor only
|
||||||
gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') {
|
gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
|
@ -441,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"
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<issue id="ObsoleteSdkInt" severity="error" />
|
<issue id="ObsoleteSdkInt" severity="error" />
|
||||||
<issue id="Recycle" severity="error" />
|
<issue id="Recycle" severity="error" />
|
||||||
<issue id="KotlinPropertyAccess" severity="error" />
|
<issue id="KotlinPropertyAccess" severity="error" />
|
||||||
|
<issue id="DefaultLocale" severity="error" />
|
||||||
|
|
||||||
<issue id="InvalidPackage">
|
<issue id="InvalidPackage">
|
||||||
<!-- Ignore error from HtmlCompressor lib -->
|
<!-- Ignore error from HtmlCompressor lib -->
|
||||||
|
@ -52,6 +53,9 @@
|
||||||
<!-- Manifest -->
|
<!-- Manifest -->
|
||||||
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
||||||
|
|
||||||
|
<!-- Dependencies -->
|
||||||
|
<issue id="KtxExtensionAvailable" severity="error" />
|
||||||
|
|
||||||
<!-- Timber -->
|
<!-- Timber -->
|
||||||
<!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
|
<!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
|
||||||
<!-- <issue id="BinaryOperationInTimber" severity="error" />-->
|
<!-- <issue id="BinaryOperationInTimber" severity="error" />-->
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.google.firebase.iid.FirebaseInstanceId
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.startAddGoogleAccountIntent
|
import im.vector.app.core.utils.startAddGoogleAccountIntent
|
||||||
|
@ -36,29 +36,33 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi
|
||||||
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
|
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
status = TestStatus.RUNNING
|
status = TestStatus.RUNNING
|
||||||
try {
|
try {
|
||||||
FirebaseInstanceId.getInstance().instanceId
|
FirebaseMessaging.getInstance().token
|
||||||
.addOnCompleteListener(context) { task ->
|
.addOnCompleteListener(context) { task ->
|
||||||
if (!task.isSuccessful) {
|
if (!task.isSuccessful) {
|
||||||
val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage
|
|
||||||
// Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
|
// Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
|
||||||
if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) {
|
description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") {
|
||||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
|
"SERVICE_NOT_AVAILABLE" -> {
|
||||||
} else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) {
|
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
|
||||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
|
}
|
||||||
} else if ("ACCOUNT_MISSING".equals(errorMsg)) {
|
"TOO_MANY_REGISTRATIONS" -> {
|
||||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
|
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
|
||||||
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
|
}
|
||||||
override fun doFix() {
|
"ACCOUNT_MISSING" -> {
|
||||||
startAddGoogleAccountIntent(context, activityResultLauncher)
|
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
|
||||||
}
|
override fun doFix() {
|
||||||
|
startAddGoogleAccountIntent(context, activityResultLauncher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
|
|
||||||
}
|
}
|
||||||
status = TestStatus.FAILED
|
status = TestStatus.FAILED
|
||||||
} else {
|
} else {
|
||||||
task.result?.token?.let { token ->
|
task.result?.let { token ->
|
||||||
val tok = token.substring(0, Math.min(8, token.length)) + "********************"
|
val tok = token.take(8) + "********************"
|
||||||
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
|
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
|
||||||
Timber.e("Retrieved FCM token success [$tok].")
|
Timber.e("Retrieved FCM token success [$tok].")
|
||||||
// Ensure it is well store in our local storage
|
// Ensure it is well store in our local storage
|
||||||
|
|
|
@ -21,7 +21,7 @@ import android.widget.Toast
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import com.google.firebase.iid.FirebaseInstanceId
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultSharedPreferences
|
||||||
|
@ -71,14 +71,16 @@ object FcmHelper {
|
||||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||||
if (checkPlayServices(activity)) {
|
if (checkPlayServices(activity)) {
|
||||||
try {
|
try {
|
||||||
FirebaseInstanceId.getInstance().instanceId
|
FirebaseMessaging.getInstance().token
|
||||||
.addOnSuccessListener(activity) { instanceIdResult ->
|
.addOnSuccessListener { token ->
|
||||||
storeFcmToken(activity, instanceIdResult.token)
|
storeFcmToken(activity, token)
|
||||||
if (registerPusher) {
|
if (registerPusher) {
|
||||||
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
|
pushersManager.registerPusherWithFcmKey(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") }
|
.addOnFailureListener { e ->
|
||||||
|
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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 timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the mimetype from a uri.
|
* Returns the mimetype from a uri.
|
||||||
|
@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
|
||||||
|
|
||||||
if (null != mimeType) {
|
if (null != mimeType) {
|
||||||
// the mimetype is sometimes in uppercase.
|
// the mimetype is sometimes in uppercase.
|
||||||
mimeType = mimeType.toLowerCase()
|
mimeType = mimeType.toLowerCase(Locale.ROOT)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to open resource input stream")
|
Timber.e(e, "Failed to open resource input stream")
|
||||||
|
|
|
@ -43,7 +43,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
|
||||||
* so you can use this in a switchMap or a flatMap
|
* so you can use this in a switchMap or a flatMap
|
||||||
*/
|
*/
|
||||||
// False positive
|
// False positive
|
||||||
@Suppress("USELESS_CAST")
|
@Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
|
||||||
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
|
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
|
||||||
setState { stateReducer(Loading()) }
|
setState { stateReducer(Loading()) }
|
||||||
return map { Success(it) as Async<T> }
|
return map { Success(it) as Async<T> }
|
||||||
|
@ -56,7 +56,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
|
||||||
* so you can use this in a switchMap or a flatMap
|
* so you can use this in a switchMap or a flatMap
|
||||||
*/
|
*/
|
||||||
// False positive
|
// False positive
|
||||||
@Suppress("USELESS_CAST")
|
@Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
|
||||||
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
|
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
|
||||||
setState { stateReducer(Loading()) }
|
setState { stateReducer(Loading()) }
|
||||||
return map { Success(it) as Async<T> }
|
return map { Success(it) as Async<T> }
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.core.ui.bottomsheet
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.app.core.platform.EmptyAction
|
||||||
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
abstract class BottomSheetGenericViewModel<State : MvRxState>(initialState: State) :
|
||||||
|
VectorViewModel<State, EmptyAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {
|
||||||
|
// No op
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableD
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun post(value: T) {
|
override fun post(value: T) {
|
||||||
behaviorRelay.accept(value)
|
behaviorRelay.accept(value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRelay(): BehaviorRelay<T> {
|
private fun createRelay(): BehaviorRelay<T> {
|
||||||
|
@ -68,6 +68,6 @@ open class PublishDataSource<T> : MutableDataSource<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun post(value: T) {
|
override fun post(value: T) {
|
||||||
publishRelay.accept(value)
|
publishRelay.accept(value!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.utils
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
// Implementation should return true in case of success
|
// Implementation should return true in case of success
|
||||||
typealias ActionOnFile = (file: File) -> Boolean
|
typealias ActionOnFile = (file: File) -> Boolean
|
||||||
|
@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? {
|
||||||
val ext = filename.substring(dotPos + 1)
|
val ext = filename.substring(dotPos + 1)
|
||||||
|
|
||||||
if (ext.isNotBlank()) {
|
if (ext.isNotBlank()) {
|
||||||
return ext.toLowerCase()
|
return ext.toLowerCase(Locale.ROOT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,6 @@ data class AttachmentsPreviewArgs(
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
class AttachmentsPreviewFragment @Inject constructor(
|
class AttachmentsPreviewFragment @Inject constructor(
|
||||||
val viewModelFactory: AttachmentsPreviewViewModel.Factory,
|
|
||||||
private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController,
|
private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController,
|
||||||
private val attachmentBigPreviewController: AttachmentBigPreviewController,
|
private val attachmentBigPreviewController: AttachmentBigPreviewController,
|
||||||
private val colorProvider: ColorProvider
|
private val colorProvider: ColorProvider
|
||||||
|
|
|
@ -17,31 +17,12 @@
|
||||||
|
|
||||||
package im.vector.app.features.attachments.preview
|
package im.vector.app.features.attachments.preview
|
||||||
|
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import com.squareup.inject.assisted.Assisted
|
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
|
||||||
class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState)
|
class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState)
|
||||||
: VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) {
|
: VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
|
||||||
interface Factory {
|
|
||||||
fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<AttachmentsPreviewViewModel, AttachmentsPreviewViewState> {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? {
|
|
||||||
val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
|
||||||
return fragment.viewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handle(action: AttachmentsPreviewAction) {
|
override fun handle(action: AttachmentsPreviewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action)
|
is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -175,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
|
||||||
|
@ -186,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
|
||||||
|
@ -195,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
|
||||||
|
@ -290,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
|
||||||
|
@ -312,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)
|
||||||
|
@ -479,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))
|
||||||
}
|
}
|
||||||
|
@ -1212,9 +1222,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
|
||||||
|
@ -1670,12 +1677,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
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) {
|
||||||
|
@ -1705,12 +1707,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) {
|
||||||
|
|
|
@ -70,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
|
||||||
|
@ -81,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
|
||||||
|
@ -93,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
|
||||||
|
@ -1019,10 +1016,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(
|
||||||
|
@ -1033,7 +1030,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,
|
||||||
|
@ -1042,12 +1039,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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,6 +84,7 @@ 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
|
||||||
|
@ -204,7 +205,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 +270,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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -77,11 +77,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
|
||||||
|
@ -142,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)
|
||||||
|
@ -196,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)
|
||||||
|
@ -227,14 +230,16 @@ 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()
|
||||||
|
|
|
@ -125,8 +125,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,
|
||||||
|
|
|
@ -74,6 +74,10 @@ class PinFragment @Inject constructor(
|
||||||
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPinCodeEnteredFirst(pinCode: String?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCodeCreated(encodedCode: String) {
|
override fun onCodeCreated(encodedCode: String) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
pinCodeStore.storeEncodedPin(encodedCode)
|
pinCodeStore.storeEncodedPin(encodedCode)
|
||||||
|
|
|
@ -37,7 +37,6 @@ class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric<RoomHistoryVisibilit
|
||||||
|
|
||||||
private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel
|
private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel
|
||||||
@Inject lateinit var controller: RoomHistoryVisibilityController
|
@Inject lateinit var controller: RoomHistoryVisibilityController
|
||||||
@Inject lateinit var roomHistoryVisibilityViewModelFactory: RoomHistoryVisibilityViewModel.Factory
|
|
||||||
private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class)
|
private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class)
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
|
|
@ -16,32 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.roomprofile.settings.historyvisibility
|
package im.vector.app.features.roomprofile.settings.historyvisibility
|
||||||
|
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import com.squareup.inject.assisted.Assisted
|
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
|
||||||
import im.vector.app.core.platform.EmptyAction
|
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
|
||||||
|
|
||||||
class RoomHistoryVisibilityViewModel @AssistedInject constructor(@Assisted initialState: RoomHistoryVisibilityState)
|
class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState)
|
||||||
: VectorViewModel<RoomHistoryVisibilityState, EmptyAction, EmptyViewEvents>(initialState) {
|
: BottomSheetGenericViewModel<RoomHistoryVisibilityState>(initialState)
|
||||||
|
|
||||||
@AssistedInject.Factory
|
|
||||||
interface Factory {
|
|
||||||
fun create(initialState: RoomHistoryVisibilityState): RoomHistoryVisibilityViewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomHistoryVisibilityViewModel, RoomHistoryVisibilityState> {
|
|
||||||
@JvmStatic
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: RoomHistoryVisibilityState): RoomHistoryVisibilityViewModel? {
|
|
||||||
val fragment: RoomHistoryVisibilityBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
|
||||||
return fragment.roomHistoryVisibilityViewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {
|
|
||||||
// No op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -39,7 +39,6 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric<RoomJoinRuleState, RoomJoinRu
|
||||||
|
|
||||||
private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel
|
private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel
|
||||||
@Inject lateinit var controller: RoomJoinRuleController
|
@Inject lateinit var controller: RoomJoinRuleController
|
||||||
@Inject lateinit var roomJoinRuleViewModelFactory: RoomJoinRuleViewModel.Factory
|
|
||||||
private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class)
|
private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class)
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
|
|
@ -16,32 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.roomprofile.settings.joinrule
|
package im.vector.app.features.roomprofile.settings.joinrule
|
||||||
|
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import com.squareup.inject.assisted.Assisted
|
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
|
||||||
import im.vector.app.core.platform.EmptyAction
|
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
|
||||||
|
|
||||||
class RoomJoinRuleViewModel @AssistedInject constructor(@Assisted initialState: RoomJoinRuleState)
|
class RoomJoinRuleViewModel(initialState: RoomJoinRuleState)
|
||||||
: VectorViewModel<RoomJoinRuleState, EmptyAction, EmptyViewEvents>(initialState) {
|
: BottomSheetGenericViewModel<RoomJoinRuleState>(initialState)
|
||||||
|
|
||||||
@AssistedInject.Factory
|
|
||||||
interface Factory {
|
|
||||||
fun create(initialState: RoomJoinRuleState): RoomJoinRuleViewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomJoinRuleViewModel, RoomJoinRuleState> {
|
|
||||||
@JvmStatic
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleState): RoomJoinRuleViewModel? {
|
|
||||||
val fragment: RoomJoinRuleBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
|
||||||
return fragment.roomJoinRuleViewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {
|
|
||||||
// No op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
9
vector/src/main/res/drawable/bg_send.xml
Normal file
9
vector/src/main/res/drawable/bg_send.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<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>
|
||||||
|
|
21
vector/src/main/res/drawable/ic_insert_emoji.xml
Normal file
21
vector/src/main/res/drawable/ic_insert_emoji.xml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportWidth="32"
|
||||||
|
android:viewportHeight="32">
|
||||||
|
<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="M9.276,19.6667C10.7004,21.8728 13.1806,23.3333 16.0019,23.3333C18.8233,23.3333 21.3035,21.8728 22.7278,19.6667"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#61708B"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.6667,12.6667a2,2.6667 0,1 0,4 0a2,2.6667 0,1 0,-4 0z"
|
||||||
|
android:fillColor="#61708B"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.6667,12.6667a2,2.6667 0,1 0,4 0a2,2.6667 0,1 0,-4 0z"
|
||||||
|
android:fillColor="#61708B"/>
|
||||||
|
</vector>
|
4
vector/src/main/res/drawable/ic_keyboard.xml
Normal file
4
vector/src/main/res/drawable/ic_keyboard.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<vector android:height="32dp" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#E3E8F0" android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM11,8h2v2h-2L11,8zM11,11h2v2h-2v-2zM8,8h2v2L8,10L8,8zM8,11h2v2L8,13v-2zM7,13L5,13v-2h2v2zM7,10L5,10L5,8h2v2zM16,17L8,17v-2h8v2zM16,13h-2v-2h2v2zM16,10h-2L14,8h2v2zM19,13h-2v-2h2v2zM19,10h-2L17,8h2v2z"/>
|
||||||
|
</vector>
|
|
@ -77,9 +77,9 @@
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
app:layout_constraintEnd_toStartOf="parent"
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="parent"
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
|
app:tint="?riotx_text_primary"
|
||||||
tools:ignore="MissingConstraints,MissingPrefix"
|
tools:ignore="MissingConstraints,MissingPrefix"
|
||||||
tools:src="@drawable/ic_edit"
|
tools:src="@drawable/ic_edit" />
|
||||||
app:tint="?riotx_text_primary" />
|
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -88,54 +88,50 @@
|
||||||
android:layout_height="22dp"
|
android:layout_height="22dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:src="@drawable/ic_close_round"
|
android:src="@drawable/ic_close_round"
|
||||||
app:tint="@color/riotx_notice"
|
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toTopOf="parent"
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="parent"
|
app:layout_constraintStart_toEndOf="parent"
|
||||||
tools:visibility="visible"
|
app:tint="@color/riotx_notice"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageButton
|
||||||
android:id="@+id/composer_avatar_view"
|
android:id="@+id/attachmentButton"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_margin="12dp"
|
||||||
android:layout_marginTop="8dp"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:layout_marginEnd="4dp"
|
android:src="@drawable/ic_attachment"
|
||||||
app:layout_goneMarginEnd="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_bias="1"
|
tools:ignore="MissingPrefix" />
|
||||||
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
|
|
||||||
tools:src="@tools:sample/avatars" />
|
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/composer_shield"
|
android:id="@+id/composer_shield"
|
||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp"
|
android:layout_height="16dp"
|
||||||
app:layout_constraintTop_toTopOf="@id/composer_avatar_view"
|
app:layout_constraintBottom_toBottomOf="@id/attachmentButton"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
|
app:layout_constraintEnd_toStartOf="@+id/attachmentButton"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composer_avatar_view"
|
app:layout_constraintStart_toEndOf="@+id/attachmentButton"
|
||||||
app:layout_constraintStart_toEndOf="@+id/composer_avatar_view"
|
app:layout_constraintTop_toTopOf="@id/attachmentButton"
|
||||||
tools:src="@drawable/ic_shield_black"
|
tools:src="@drawable/ic_shield_black"
|
||||||
tools:visibility="visible"
|
tools:visibility="visible" />
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentButton"
|
android:id="@+id/composer_emoji"
|
||||||
android:layout_width="48dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_insert_emoji"
|
||||||
app:tint="?attr/colorAccent"
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
app:layout_constraintStart_toEndOf="@id/composerEditText"
|
app:layout_constraintStart_toEndOf="@id/composerEditText"
|
||||||
tools:ignore="MissingPrefix" />
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_goneMarginEnd="8dp" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/composer_preview_barrier"
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
@ -149,14 +145,15 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="36dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:background="@drawable/bg_send"
|
||||||
android:src="@drawable/ic_send"
|
android:src="@drawable/ic_send"
|
||||||
app:tint="?attr/colorAccent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
app:layout_constraintStart_toEndOf="@id/composer_emoji"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.ComposerEditText
|
<im.vector.app.features.home.room.detail.composer.ComposerEditText
|
||||||
|
@ -168,7 +165,7 @@
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
android:nextFocusUp="@id/composerEditText"
|
android:nextFocusUp="@id/composerEditText"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/attachmentButton"
|
app:layout_constraintEnd_toStartOf="@+id/composer_emoji"
|
||||||
app:layout_constraintStart_toEndOf="@+id/composer_shield"
|
app:layout_constraintStart_toEndOf="@+id/composer_shield"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="@tools:sample/lorem/random" />
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
|
@ -83,9 +83,9 @@
|
||||||
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
|
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
|
||||||
app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view"
|
app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view"
|
||||||
app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
|
app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
|
||||||
tools:src="@drawable/ic_edit"
|
|
||||||
app:tint="?riotx_text_primary"
|
app:tint="?riotx_text_primary"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -94,51 +94,47 @@
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:src="@drawable/ic_close_round"
|
android:src="@drawable/ic_close_round"
|
||||||
app:tint="@color/riotx_notice"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview"
|
app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/composer_related_message_preview"
|
app:layout_constraintTop_toTopOf="@id/composer_related_message_preview"
|
||||||
|
app:tint="@color/riotx_notice"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/composer_avatar_view"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
app:layout_goneMarginEnd="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
tools:src="@tools:sample/avatars" />
|
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/composer_shield"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/composer_avatar_view"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composer_avatar_view"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/composer_avatar_view"
|
|
||||||
tools:src="@drawable/ic_shield_black" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentButton"
|
android:id="@+id/attachmentButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_attachment"
|
||||||
app:tint="?attr/colorAccent"
|
app:layout_constraintStart_toStartOf="@id/composerEditText"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
|
||||||
app:layout_constraintTop_toBottomOf="parent"
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_shield"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composer_emoji"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composer_emoji"
|
||||||
|
tools:src="@drawable/ic_shield_black" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composer_emoji"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_insert_emoji"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composerEditText"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_goneMarginEnd="8dp" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/composer_preview_barrier"
|
android:id="@+id/composer_preview_barrier"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -151,11 +147,13 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="36dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="@drawable/bg_send"
|
||||||
android:src="@drawable/ic_send"
|
android:src="@drawable/ic_send"
|
||||||
app:tint="?attr/colorAccent"
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
@ -170,8 +168,8 @@
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
android:nextFocusUp="@id/composerEditText"
|
android:nextFocusUp="@id/composerEditText"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
app:layout_constraintEnd_toStartOf="@+id/composer_emoji"
|
||||||
app:layout_constraintStart_toEndOf="@id/composer_shield"
|
app:layout_constraintStart_toEndOf="@+id/composer_shield"
|
||||||
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
tools:text="@tools:sample/lorem/random" />
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,8 @@
|
||||||
android:id="@+id/composer_related_message_action_image"
|
android:id="@+id/composer_related_message_action_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
tools:ignore="MissingConstraints,MissingPrefix"
|
app:tint="?riotx_text_primary"
|
||||||
app:tint="?riotx_text_primary" />
|
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/composer_related_message_close"
|
android:id="@+id/composer_related_message_close"
|
||||||
|
@ -83,13 +83,13 @@
|
||||||
android:tint="@color/riotx_notice"
|
android:tint="@color/riotx_notice"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
<ImageView
|
android:id="@+id/composer_emoji"
|
||||||
android:id="@+id/composer_avatar_view"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
tools:ignore="MissingConstraints"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
tools:src="@tools:sample/avatars" />
|
android:src="@drawable/ic_insert_emoji"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentButton"
|
android:id="@+id/attachmentButton"
|
||||||
|
@ -98,7 +98,6 @@
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:contentDescription="@string/option_send_files"
|
android:contentDescription="@string/option_send_files"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_attachment"
|
||||||
android:tint="?attr/colorAccent"
|
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
@ -115,10 +114,9 @@
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="@drawable/bg_send"
|
||||||
android:contentDescription="@string/send"
|
android:contentDescription="@string/send"
|
||||||
android:src="@drawable/ic_send"
|
android:src="@drawable/ic_send"
|
||||||
android:tint="?attr/colorAccent"
|
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.ComposerEditText
|
<im.vector.app.features.home.room.detail.composer.ComposerEditText
|
||||||
|
|
Loading…
Reference in a new issue