forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/Perf
This commit is contained in:
@ -5,7 +5,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@ -14,21 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.glide;
|
||||
package im.vector.riotredesign.core.glide
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.bumptech.glide.load.Option
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt"
|
||||
|
||||
@GlideModule
|
||||
public final class MyAppGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setLogLevel(Log.ERROR);
|
||||
}
|
||||
val ELEMENT_TO_DECRYPT = Option.memory(
|
||||
ElementToDecryptOptionKey, ElementToDecrypt("", "", ""))
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import im.vector.riotredesign.core.extensions.vectorComponent
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import java.io.InputStream
|
||||
|
||||
@GlideModule
|
||||
class MyAppGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
builder.setLogLevel(Log.ERROR)
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.append(ImageContentRenderer.Data::class.java,
|
||||
InputStream::class.java,
|
||||
VectorGlideModelLoaderFactory(context.vectorComponent().activeSessionHolder()))
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import com.bumptech.glide.load.engine.Resource as Resource1
|
||||
|
||||
|
||||
class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder)
|
||||
: ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||
return VectorGlideModelLoader(activeSessionHolder)
|
||||
}
|
||||
|
||||
override fun teardown() {
|
||||
// Is there something to do here?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder)
|
||||
: ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||
override fun handles(model: ImageContentRenderer.Data): Boolean {
|
||||
// Always handle
|
||||
return true
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
|
||||
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(activeSessionHolder, model, width, height))
|
||||
}
|
||||
}
|
||||
|
||||
class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val data: ImageContentRenderer.Data,
|
||||
private val width: Int,
|
||||
private val height: Int)
|
||||
: DataFetcher<InputStream> {
|
||||
|
||||
val client = OkHttpClient()
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
private var stream: InputStream? = null
|
||||
|
||||
override fun cleanup() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
// ?
|
||||
return DataSource.REMOTE
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream?.close() // interrupts decode if any
|
||||
stream = null
|
||||
} catch (ignore: IOException) {
|
||||
Timber.e(ignore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
Timber.v("Load data: $data")
|
||||
if (data.isLocalFile()) {
|
||||
val initialFile = File(data.url)
|
||||
callback.onDataReady(FileInputStream(initialFile))
|
||||
return
|
||||
}
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val url = contentUrlResolver.resolveFullSize(data.url)
|
||||
?: return
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val inputStream = response.body()?.byteStream()
|
||||
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
|
||||
if (!response.isSuccessful) {
|
||||
callback.onLoadFailed(IOException("Unexpected code $response"))
|
||||
return
|
||||
}
|
||||
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
|
||||
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
|
||||
} else {
|
||||
inputStream
|
||||
}
|
||||
callback.onDataReady(stream)
|
||||
}
|
||||
}
|
@ -21,9 +21,9 @@ import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
|
||||
fun getFilenameFromUri(context: Context, uri: Uri): String? {
|
||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||
var result: String? = null
|
||||
if (uri.scheme == "content") {
|
||||
if (context != null && uri.scheme == "content") {
|
||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
|
@ -1,592 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.security.KeyPairGeneratorSpec
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.*
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
|
||||
/**
|
||||
* Offers simple methods to securely store secrets in an Android Application.
|
||||
* The encryption keys are randomly generated and securely managed by the key store, thus your secrets
|
||||
* are safe. You only need to remember a key alias to perform encrypt/decrypt operations.
|
||||
*
|
||||
* <b>Android M++</b>
|
||||
* On android M+, the keystore can generates and store AES keys via API. But below API M this functionality
|
||||
* is not available.
|
||||
*
|
||||
* <b>Android [K-M[</b>
|
||||
* For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
|
||||
* random secret key in generated to perform encryption.
|
||||
* This secret key is encrypted with the public RSA key and stored with the encrypted secret.
|
||||
* In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
|
||||
*
|
||||
* <b>Older androids</b>
|
||||
* For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
|
||||
* The salt and iv are stored with encrypted data.
|
||||
*
|
||||
* Sample usage:
|
||||
* <code>
|
||||
* val secret = "The answer is 42"
|
||||
* val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
|
||||
* //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
|
||||
*
|
||||
* //to get back the secret, just call
|
||||
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
|
||||
* </code>
|
||||
*
|
||||
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
|
||||
*
|
||||
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
|
||||
* add a pin or change the schema); So you might and with a useless pile of bytes.
|
||||
*/
|
||||
object SecretStoringUtils {
|
||||
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
|
||||
|
||||
const val FORMAT_API_M: Byte = 0
|
||||
const val FORMAT_1: Byte = 1
|
||||
const val FORMAT_2: Byte = 2
|
||||
|
||||
val keyStore: KeyStore by lazy {
|
||||
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
||||
load(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val secureRandom = SecureRandom()
|
||||
|
||||
/**
|
||||
* Encrypt the given secret using the android Keystore.
|
||||
* On android >= M, will directly use the keystore to generate a symetric key
|
||||
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
|
||||
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
|
||||
* in the bytearray (in can be stored anywhere, it is encrypted)
|
||||
* On older version a key in generated from alias with random salt.
|
||||
*
|
||||
* The secret is encrypted using the following method: AES/GCM/NoPadding
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return encryptStringM(secret, keyAlias)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return encryptStringJ(secret, keyAlias, context)
|
||||
} else {
|
||||
return encryptForOldDevicesNotGood(secret, keyAlias)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a secret that was encrypted by #securelyStoreString()
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return decryptStringM(encrypted, keyAlias)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return decryptStringJ(encrypted, keyAlias, context)
|
||||
} else {
|
||||
return decryptForOldDevicesNotGood(encrypted, keyAlias)
|
||||
}
|
||||
}
|
||||
|
||||
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
saveSecureObjectM(keyAlias, output, any)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return saveSecureObjectK(keyAlias, output, any, context)
|
||||
} else {
|
||||
return saveSecureObjectOldNotGood(keyAlias, output, any)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return loadSecureObjectM(keyAlias, inputStream)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return loadSecureObjectK(keyAlias, inputStream, context)
|
||||
} else {
|
||||
return loadSecureObjectOldNotGood(keyAlias, inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
|
||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||
?.secretKey
|
||||
if (secretKeyEntry == null) {
|
||||
//we generate it
|
||||
val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||
val keyGenSpec = KeyGenParameterSpec.Builder(alias,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setKeySize(128)
|
||||
.build()
|
||||
generator.init(keyGenSpec)
|
||||
return generator.generateKey()
|
||||
}
|
||||
return secretKeyEntry
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Symetric Key Generation is only available in M, so before M the idea is to:
|
||||
- Generate a pair of RSA keys;
|
||||
- Generate a random AES key;
|
||||
- Encrypt the AES key using the RSA public key;
|
||||
- Store the encrypted AES
|
||||
Generate a key pair for encryption
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
|
||||
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
|
||||
|
||||
if (privateKeyEntry != null) return privateKeyEntry
|
||||
|
||||
val start = Calendar.getInstance()
|
||||
val end = Calendar.getInstance()
|
||||
end.add(Calendar.YEAR, 30)
|
||||
|
||||
val spec = KeyPairGeneratorSpec.Builder(context)
|
||||
.setAlias(alias)
|
||||
.setSubject(X500Principal("CN=$alias"))
|
||||
.setSerialNumber(BigInteger.TEN)
|
||||
//.setEncryptionRequired() requires that the phone as a pin/schema
|
||||
.setStartDate(start.time)
|
||||
.setEndDate(end.time)
|
||||
.build()
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE).run {
|
||||
initialize(spec)
|
||||
generateKeyPair()
|
||||
}
|
||||
return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
val iv = cipher.iv
|
||||
//we happen the iv to the final result
|
||||
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
||||
return formatMMake(iv, encryptedBytes)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
|
||||
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
|
||||
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
|
||||
//we generate a random symetric key
|
||||
val key = ByteArray(16)
|
||||
secureRandom.nextBytes(key)
|
||||
val sKey = SecretKeySpec(key, "AES")
|
||||
|
||||
//we encrypt this key thanks to the key store
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key, context)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
val iv = cipher.iv
|
||||
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
||||
|
||||
return format1Make(encryptedKey, iv, encryptedBytes)
|
||||
}
|
||||
|
||||
fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
|
||||
val salt = ByteArray(8)
|
||||
secureRandom.nextBytes(salt)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
val sKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
val iv = cipher.iv
|
||||
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
||||
|
||||
return format2Make(salt, iv, encryptedBytes)
|
||||
}
|
||||
|
||||
fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
|
||||
|
||||
val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
val sKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
// cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
||||
|
||||
val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, sKey, specIV)
|
||||
|
||||
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
|
||||
|
||||
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
|
||||
|
||||
//we need to decrypt the key
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
||||
|
||||
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Throws(IOException::class)
|
||||
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
|
||||
val iv = cipher.iv
|
||||
|
||||
val bos1 = ByteArrayOutputStream()
|
||||
ObjectOutputStream(bos1).use {
|
||||
it.writeObject(writeObject)
|
||||
}
|
||||
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
|
||||
val doFinal = cipher.doFinal(bos1.toByteArray())
|
||||
output.write(FORMAT_API_M.toInt())
|
||||
output.write(iv.size)
|
||||
output.write(iv)
|
||||
output.write(doFinal)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
|
||||
//we generate a random symetric key
|
||||
val key = ByteArray(16)
|
||||
secureRandom.nextBytes(key)
|
||||
val sKey = SecretKeySpec(key, "AES")
|
||||
|
||||
//we encrypt this key thanks to the key store
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key, context)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
val iv = cipher.iv
|
||||
|
||||
val bos1 = ByteArrayOutputStream()
|
||||
val cos = CipherOutputStream(bos1, cipher)
|
||||
ObjectOutputStream(cos).use {
|
||||
it.writeObject(writeObject)
|
||||
}
|
||||
|
||||
output.write(FORMAT_1.toInt())
|
||||
output.write((encryptedKey.size and 0xFF00).shr(8))
|
||||
output.write(encryptedKey.size and 0x00FF)
|
||||
output.write(encryptedKey)
|
||||
output.write(iv.size)
|
||||
output.write(iv)
|
||||
output.write(bos1.toByteArray())
|
||||
}
|
||||
|
||||
fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
val salt = ByteArray(8)
|
||||
secureRandom.nextBytes(salt)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
|
||||
val secretKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
val iv = cipher.iv
|
||||
|
||||
val bos1 = ByteArrayOutputStream()
|
||||
ObjectOutputStream(bos1).use {
|
||||
it.writeObject(writeObject)
|
||||
}
|
||||
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
|
||||
val doFinal = cipher.doFinal(bos1.toByteArray())
|
||||
|
||||
output.write(FORMAT_2.toInt())
|
||||
output.write(salt.size)
|
||||
output.write(salt)
|
||||
output.write(iv.size)
|
||||
output.write(iv)
|
||||
output.write(doFinal)
|
||||
}
|
||||
|
||||
// @RequiresApi(Build.VERSION_CODES.M)
|
||||
// @Throws(IOException::class)
|
||||
// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
|
||||
// FileOutputStream(file).use {
|
||||
// saveSecureObjectM(keyAlias, it, writeObject)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @RequiresApi(Build.VERSION_CODES.M)
|
||||
// @Throws(IOException::class)
|
||||
// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? {
|
||||
// FileInputStream(file).use {
|
||||
// return loadSecureObjectM<T>(keyAlias, it)
|
||||
// }
|
||||
// }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Throws(IOException::class)
|
||||
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
|
||||
val format = inputStream.read()
|
||||
assert(format.toByte() == FORMAT_API_M)
|
||||
|
||||
val ivSize = inputStream.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
inputStream.read(iv, 0, ivSize)
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
CipherInputStream(inputStream, cipher).use { cipherInputStream ->
|
||||
ObjectInputStream(cipherInputStream).use {
|
||||
val readObject = it.readObject()
|
||||
return readObject as? T
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
@Throws(IOException::class)
|
||||
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
|
||||
|
||||
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
|
||||
|
||||
//we need to decrypt the key
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
||||
|
||||
val encIS = ByteArrayInputStream(encrypted)
|
||||
|
||||
CipherInputStream(encIS, cipher).use { cipherInputStream ->
|
||||
ObjectInputStream(cipherInputStream).use {
|
||||
val readObject = it.readObject()
|
||||
return readObject as? T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
|
||||
|
||||
val (salt, iv, encrypted) = format2Extract(inputStream)
|
||||
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
|
||||
val sKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
//we need to decrypt the key
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, sKey, spec)
|
||||
|
||||
val encIS = ByteArrayInputStream(encrypted)
|
||||
|
||||
CipherInputStream(encIS, cipher).use {
|
||||
ObjectInputStream(it).use {
|
||||
val readObject = it.readObject()
|
||||
return readObject as? T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Throws(Exception::class)
|
||||
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
|
||||
// Encrypt the text
|
||||
val inputCipher = Cipher.getInstance(RSA_MODE)
|
||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
|
||||
cipherOutputStream.write(secret)
|
||||
cipherOutputStream.close()
|
||||
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Throws(Exception::class)
|
||||
private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
|
||||
val output = Cipher.getInstance(RSA_MODE)
|
||||
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
CipherInputStream(encrypted, output).use {
|
||||
it.copyTo(bos)
|
||||
}
|
||||
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
||||
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
|
||||
val format = bis.read().toByte()
|
||||
assert(format == FORMAT_API_M)
|
||||
|
||||
val ivSize = bis.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv, 0, ivSize)
|
||||
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
return Pair(iv, encrypted)
|
||||
}
|
||||
|
||||
private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray {
|
||||
val bos = ByteArrayOutputStream(2 + iv.size + data.size)
|
||||
bos.write(FORMAT_API_M.toInt())
|
||||
bos.write(iv.size)
|
||||
bos.write(iv)
|
||||
bos.write(data)
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
||||
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_1)
|
||||
|
||||
val keySizeBig = bis.read()
|
||||
val keySizeLow = bis.read()
|
||||
val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
|
||||
val encryptedKey = ByteArray(encryptedKeySize)
|
||||
bis.read(encryptedKey)
|
||||
|
||||
val ivSize = bis.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
return Triple(encryptedKey, iv, encrypted)
|
||||
}
|
||||
|
||||
private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
|
||||
val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size)
|
||||
bos.write(FORMAT_1.toInt())
|
||||
bos.write((encryptedKey.size and 0xFF00).shr(8))
|
||||
bos.write(encryptedKey.size and 0x00FF)
|
||||
bos.write(encryptedKey)
|
||||
bos.write(iv.size)
|
||||
bos.write(iv)
|
||||
bos.write(encryptedBytes)
|
||||
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
||||
private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
|
||||
val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
|
||||
bos.write(FORMAT_2.toInt())
|
||||
bos.write(salt.size)
|
||||
bos.write(salt)
|
||||
bos.write(iv.size)
|
||||
bos.write(iv)
|
||||
bos.write(encryptedBytes)
|
||||
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
||||
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_2)
|
||||
|
||||
val saltSize = bis.read()
|
||||
val salt = ByteArray(saltSize)
|
||||
bis.read(salt)
|
||||
|
||||
val ivSize = bis.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
return Triple(salt, iv, encrypted)
|
||||
}
|
||||
}
|
@ -616,11 +616,11 @@ class RoomDetailFragment :
|
||||
}
|
||||
|
||||
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
vectorBaseActivity.notImplemented("open file")
|
||||
}
|
||||
|
||||
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
vectorBaseActivity.notImplemented("open audio file")
|
||||
}
|
||||
|
||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@ -36,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.intent.getFilenameFromUri
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
val attachments = action.mediaFiles.map {
|
||||
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
|
||||
|
||||
ContentAttachmentData(
|
||||
size = it.size,
|
||||
duration = it.duration,
|
||||
date = it.date,
|
||||
height = it.height,
|
||||
width = it.width,
|
||||
name = it.name,
|
||||
name = nameWithExtension ?: it.name,
|
||||
path = it.path,
|
||||
mimeType = it.mimeType,
|
||||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
||||
@ -376,7 +380,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
displayedEventsObservable.accept(action)
|
||||
if (action.event.sendState.isSent()) { //ignore pending/local events
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
//We need to update this with the related m.replace also (to move read receipt)
|
||||
action.event.annotations?.editSummary?.sourceEvents?.forEach {
|
||||
room.getTimeLineEvent(it)?.let { event ->
|
||||
|
@ -299,9 +299,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||
collapsedEventIds.removeAll(mergedEventIds)
|
||||
}
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it }
|
||||
MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
|
||||
(MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
|
||||
mergeItemCollapseStates[event.localId] = it
|
||||
requestModelBuild()
|
||||
}).also {
|
||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.riotredesign.EmojiCompatFontProvider
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
@ -90,7 +91,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.unsignedData?.redactedEvent != null) {
|
||||
if (event.root.isRedacted()) {
|
||||
//message is redacted
|
||||
return buildRedactedItem(informationData, highlight, callback)
|
||||
}
|
||||
@ -198,7 +199,8 @@ class MessageItemFactory @Inject constructor(
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
val data = ImageContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
url = messageContent.url,
|
||||
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
height = messageContent.info?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
@ -239,10 +241,11 @@ class MessageItemFactory @Inject constructor(
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
val thumbnailData = ImageContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
url = messageContent.info?.thumbnailUrl,
|
||||
height = messageContent.info?.height,
|
||||
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
|
||||
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||
height = messageContent.videoInfo?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
width = messageContent.videoInfo?.width,
|
||||
maxWidth = maxWidth
|
||||
)
|
||||
|
||||
|
@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
EventType.ENCRYPTED -> {
|
||||
if (event.root.isRedacted()) {
|
||||
// Redacted event, let the MessageItemFactory handle it
|
||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
} else {
|
||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
|
@ -31,4 +31,19 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class MergedTimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||
private val events: List<TimelineEvent>)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
events.forEach {
|
||||
callback?.onEventVisible(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -22,8 +22,8 @@ import android.widget.ImageView
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||
@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
data class Data(
|
||||
val filename: String,
|
||||
val url: String?,
|
||||
val elementToDecrypt: ElementToDecrypt?,
|
||||
val height: Int?,
|
||||
val maxHeight: Int,
|
||||
val width: Int?,
|
||||
@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
val (width, height) = processSize(data, mode)
|
||||
imageView.layoutParams.height = height
|
||||
imageView.layoutParams.width = width
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
val glideRequest = if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
}
|
||||
|
||||
glideRequest
|
||||
.dontAnimate()
|
||||
.transform(RoundedCorners(dpToPx(8, imageView.context)))
|
||||
.thumbnail(0.3f)
|
||||
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
// TODO DECRYPT_FILE Decrypt file
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail),
|
||||
Uri.parse(fullSize)
|
||||
|
@ -20,9 +20,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
||||
import com.github.piasy.biv.view.GlideImageViewFactory
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
|
||||
import javax.inject.Inject
|
||||
@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
|
||||
finish()
|
||||
} else {
|
||||
configureToolbar(imageMediaViewerToolbar, mediaData)
|
||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||
|
||||
if (mediaData.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
imageMediaViewerImageView.isVisible = false
|
||||
encryptedImageView.isVisible = true
|
||||
|
||||
GlideApp
|
||||
.with(this)
|
||||
.load(mediaData)
|
||||
.dontAnimate()
|
||||
.into(encryptedImageView)
|
||||
} else {
|
||||
// Clear image
|
||||
imageMediaViewerImageView.isVisible = true
|
||||
encryptedImageView.isVisible = false
|
||||
|
||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ import javax.inject.Inject
|
||||
|
||||
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
|
||||
|
||||
// TODO DECRYPT_FILE Encrypted data
|
||||
@Parcelize
|
||||
data class Data(
|
||||
val filename: String,
|
||||
|
@ -21,10 +21,10 @@ import android.graphics.Bitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.util.SecretStoringUtils
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.utils.SecretStoringUtils
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import me.gujun.android.span.span
|
||||
import timber.log.Timber
|
||||
|
@ -32,6 +32,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.riotredesign.R
|
||||
@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.MainActivity
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||
|
||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
notImplemented()
|
||||
|
||||
// TODO DECRYPT_FILE Quick implementation of clear cache, finish this
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// On UI Thread
|
||||
Glide.get(requireContext()).clearMemory()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// On BG thread
|
||||
Glide.get(requireContext()).clearDiskCache()
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO
|
||||
displayLoadingView()
|
||||
|
||||
|
@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
|
||||
context?.let { context: Context ->
|
||||
AlertDialog.Builder(context)
|
||||
.setSingleChoiceItems(R.array.media_saving_choice,
|
||||
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
||||
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
||||
PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
|
||||
d.cancel()
|
||||
|
||||
|
@ -18,6 +18,7 @@ package im.vector.riotredesign.features.workers.signout
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@ -41,16 +42,17 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.di.DaggerScreenComponent
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
@Inject lateinit var session: Session
|
||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
lateinit var session: Session
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_warning_text)
|
||||
@ -99,6 +101,14 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var viewModel: SignOutViewModel
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
val vectorBaseActivity = activity as VectorBaseActivity
|
||||
val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
||||
viewModelFactory = screenComponent.viewModelFactory()
|
||||
session = screenComponent.session()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@ -18,4 +19,11 @@
|
||||
app:failureImageInitScaleType="center"
|
||||
app:optimizeDisplay="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/encryptedImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
Reference in New Issue
Block a user