mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
4 Commits
v1.6.46
...
feature/bc
Author | SHA1 | Date | |
---|---|---|---|
|
259f4de37f | ||
|
e26cbf24dd | ||
|
ecaf20a20f | ||
|
f9a4d95913 |
@@ -142,6 +142,7 @@ dependencies {
|
||||
|
||||
// Image
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||
implementation 'io.trbl:blurhash:1.0.0'
|
||||
|
||||
// Database
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
||||
|
@@ -287,4 +287,6 @@ interface Session :
|
||||
* Maintenance API, allows to print outs info on DB size to logcat
|
||||
*/
|
||||
fun logDbUsageInfo()
|
||||
|
||||
fun storageUsageService() : StorageUsageService
|
||||
}
|
||||
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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 org.matrix.android.sdk.api.session
|
||||
|
||||
/**
|
||||
* Some utility to check storage usage.
|
||||
*/
|
||||
interface StorageUsageService {
|
||||
|
||||
/** Size in bytes */
|
||||
fun sessionDataBaseSize(): Long
|
||||
|
||||
/** Size in bytes */
|
||||
fun cryptoDataBaseSize(): Long
|
||||
|
||||
fun cacheDirectorySize(folderName: String) : Long
|
||||
}
|
@@ -55,7 +55,13 @@ data class ImageInfo(
|
||||
/**
|
||||
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||
*/
|
||||
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null,
|
||||
|
||||
/**
|
||||
* MSC2448: BlurHash is a compact representation of a placeholder for an image (or a frame of video)
|
||||
*/
|
||||
@Json(name = "blurhash") val blurHash: String? = null
|
||||
|
||||
)
|
||||
|
||||
/**
|
||||
|
@@ -60,7 +60,12 @@ data class VideoInfo(
|
||||
/**
|
||||
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||
*/
|
||||
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null,
|
||||
|
||||
/**
|
||||
* MSC2448: BlurHash is a compact representation of a placeholder for an image (or a frame of video)
|
||||
*/
|
||||
@Json(name = "blurhash") val blurHash: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
|
@@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
@@ -155,6 +156,11 @@ internal class DefaultFileService @Inject constructor(
|
||||
}
|
||||
}
|
||||
if (!decryptSuccess) {
|
||||
// the cached file might be corrupted? what should we do here :/
|
||||
// For now delete, but could be nice to have a hash to check
|
||||
tryOrNull {
|
||||
cachedFiles.decryptedFile.delete()
|
||||
}
|
||||
throw IllegalStateException("Decryption error")
|
||||
}
|
||||
} else {
|
||||
|
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.federation.FederationService
|
||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.SessionLifecycleObserver
|
||||
import org.matrix.android.sdk.api.session.StorageUsageService
|
||||
import org.matrix.android.sdk.api.session.account.AccountService
|
||||
import org.matrix.android.sdk.api.session.accountdata.AccountDataService
|
||||
import org.matrix.android.sdk.api.session.cache.CacheService
|
||||
@@ -128,6 +129,7 @@ internal class DefaultSession @Inject constructor(
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
private val spaceService: Lazy<SpaceService>,
|
||||
private val openIdService: Lazy<OpenIdService>,
|
||||
private val storageUsageService: Lazy<StorageUsageService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||
) : Session,
|
||||
@@ -327,4 +329,8 @@ internal class DefaultSession @Inject constructor(
|
||||
override fun logDbUsageInfo() {
|
||||
RealmDebugTools(realmConfiguration).logInfo("Session")
|
||||
}
|
||||
|
||||
override fun storageUsageService(): StorageUsageService {
|
||||
return storageUsageService.get()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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 org.matrix.android.sdk.internal.session
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.StorageUsageService
|
||||
import org.matrix.android.sdk.internal.di.CacheDirectory
|
||||
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultStorageUsageService @Inject constructor(
|
||||
@SessionFilesDirectory val directory: File,
|
||||
@CacheDirectory val cacheFile: File
|
||||
) : StorageUsageService {
|
||||
|
||||
// "disk_store.realm"
|
||||
// "crypto_store.realm"
|
||||
|
||||
override fun sessionDataBaseSize(): Long {
|
||||
return tryOrNull { File(directory, "disk_store.realm").length() } ?: 0
|
||||
}
|
||||
|
||||
override fun cryptoDataBaseSize(): Long {
|
||||
return tryOrNull { File(directory, "crypto_store.realm").length() } ?: 0
|
||||
}
|
||||
|
||||
override fun cacheDirectorySize(folderName: String): Long {
|
||||
return tryOrNull {
|
||||
File(cacheFile, folderName)
|
||||
.walkTopDown()
|
||||
.onEnter {
|
||||
true
|
||||
}
|
||||
.sumOf { it.length() }
|
||||
} ?: 0L
|
||||
}
|
||||
}
|
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.auth.data.sessionId
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.SessionLifecycleObserver
|
||||
import org.matrix.android.sdk.api.session.StorageUsageService
|
||||
import org.matrix.android.sdk.api.session.accountdata.AccountDataService
|
||||
import org.matrix.android.sdk.api.session.events.EventService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
@@ -386,4 +387,7 @@ internal abstract class SessionModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
||||
|
||||
@Binds
|
||||
abstract fun bindStorageUsageService(processor: DefaultStorageUsageService): StorageUsageService
|
||||
}
|
||||
|
@@ -17,11 +17,13 @@
|
||||
package org.matrix.android.sdk.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import io.trbl.blurhash.BlurHash
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
@@ -56,6 +58,7 @@ import javax.inject.Inject
|
||||
private data class NewAttachmentAttributes(
|
||||
val newWidth: Int? = null,
|
||||
val newHeight: Int? = null,
|
||||
val blurHash: String? = null,
|
||||
val newFileSize: Long
|
||||
)
|
||||
|
||||
@@ -154,6 +157,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
var newAttachmentAttributes = NewAttachmentAttributes(
|
||||
params.attachment.width?.toInt(),
|
||||
params.attachment.height?.toInt(),
|
||||
null,
|
||||
params.attachment.size
|
||||
)
|
||||
|
||||
@@ -167,11 +171,25 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
.also { compressedFile ->
|
||||
// Get new Bitmap size
|
||||
compressedFile.inputStream().use {
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
val options = BitmapFactory.Options() // .apply { inJustDecodeBounds = true }
|
||||
val bitmap = BitmapFactory.decodeStream(it, null, options)
|
||||
val blurHash = if (bitmap != null) {
|
||||
tryOrNull {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
if (bitmap.config != Bitmap.Config.ARGB_8888) {
|
||||
bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
} else {
|
||||
bitmap
|
||||
}.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
BlurHash.encode(pixels, width, height, 4, 4)
|
||||
}
|
||||
} else null
|
||||
newAttachmentAttributes = NewAttachmentAttributes(
|
||||
newWidth = options.outWidth,
|
||||
newHeight = options.outHeight,
|
||||
blurHash = blurHash,
|
||||
newFileSize = compressedFile.length()
|
||||
)
|
||||
}
|
||||
@@ -410,7 +428,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
info = info?.copy(
|
||||
width = newAttachmentAttributes?.newWidth ?: info.width,
|
||||
height = newAttachmentAttributes?.newHeight ?: info.height,
|
||||
size = newAttachmentAttributes?.newFileSize ?: info.size
|
||||
size = newAttachmentAttributes?.newFileSize ?: info.size,
|
||||
blurHash = newAttachmentAttributes?.blurHash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@@ -169,7 +169,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
||||
}
|
||||
|
||||
fun getAllFailedEventsToResend(roomId: String): List<TimelineEvent> {
|
||||
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
|
||||
return getAllEventsWithStates(roomId, listOf(SendState.UNDELIVERED, SendState.FAILED_UNKNOWN_DEVICES, SendState.SENT))
|
||||
}
|
||||
|
||||
fun getAllEventsWithStates(roomId: String, states: List<SendState>): List<TimelineEvent> {
|
||||
|
@@ -410,6 +410,7 @@ dependencies {
|
||||
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
|
||||
implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version"
|
||||
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
|
||||
implementation 'xyz.belvi.blurHash:blurHash:1.0.4'
|
||||
|
||||
// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
@@ -122,6 +122,7 @@ import im.vector.app.features.settings.VectorSettingsPreferencesFragment
|
||||
import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment
|
||||
import im.vector.app.features.settings.account.deactivation.DeactivateAccountFragment
|
||||
import im.vector.app.features.settings.crosssigning.CrossSigningSettingsFragment
|
||||
import im.vector.app.features.settings.data.DataAndStorageFragment
|
||||
import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment
|
||||
import im.vector.app.features.settings.devtools.AccountDataFragment
|
||||
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailFragment
|
||||
@@ -804,4 +805,9 @@ interface FragmentModule {
|
||||
@IntoMap
|
||||
@FragmentKey(SpaceManageRoomsFragment::class)
|
||||
fun bindSpaceManageRoomsFragment(fragment: SpaceManageRoomsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(DataAndStorageFragment::class)
|
||||
fun bindDataAndStorageFragment(fragment: DataAndStorageFragment): Fragment
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import xyz.belvi.blurhash.BlurHash
|
||||
|
||||
@Module
|
||||
object ScreenModule {
|
||||
@@ -33,4 +34,9 @@ object ScreenModule {
|
||||
@JvmStatic
|
||||
@ScreenScope
|
||||
fun providesSharedViewPool() = RecyclerView.RecycledViewPool()
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@ScreenScope
|
||||
fun providesBlurHash(context: AppCompatActivity) = BlurHash(context, 20, 1f)
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -29,7 +30,9 @@ class LocalFilesHelper @Inject constructor(private val context: Context) {
|
||||
?.let { Uri.parse(it) }
|
||||
?.let { DocumentFile.fromSingleUri(context, it) }
|
||||
?.exists()
|
||||
.orFalse()
|
||||
.orFalse().also {
|
||||
Timber.v("## Load data: is Local file $fileUri: $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun openInputStream(fileUri: String?): InputStream? {
|
||||
|
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
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.app.R
|
||||
import im.vector.app.core.extensions.vectorComponent
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import xyz.belvi.blurhash.BlurHashDecoder
|
||||
|
||||
data class BlurHashData(
|
||||
val blurHash: String? = null
|
||||
)
|
||||
|
||||
class BlurHashModelLoaderFactory(private val context: Context) : ModelLoaderFactory<BlurHashData, Drawable> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<BlurHashData, Drawable> {
|
||||
return BlurHashModelLoader(context)
|
||||
}
|
||||
|
||||
override fun teardown() {
|
||||
// nop??
|
||||
}
|
||||
}
|
||||
|
||||
class BlurHashModelLoader(val context: Context)
|
||||
: ModelLoader<BlurHashData, Drawable> {
|
||||
|
||||
private val activeSessionHolder = context.vectorComponent().activeSessionHolder()
|
||||
|
||||
override fun buildLoadData(model: BlurHashData, width: Int, height: Int, options: Options): ModelLoader.LoadData<Drawable> {
|
||||
return ModelLoader.LoadData(ObjectKey(model),
|
||||
BlurhashDataFetcher(context,
|
||||
activeSessionHolder.getSafeActiveSession()?.coroutineScope ?: GlobalScope,
|
||||
model.blurHash,
|
||||
width,
|
||||
height
|
||||
))
|
||||
}
|
||||
|
||||
override fun handles(model: BlurHashData): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class BlurhashDataFetcher(private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val hash: String?,
|
||||
private val width: Int,
|
||||
private val height: Int)
|
||||
: DataFetcher<Drawable> {
|
||||
|
||||
var job: Job? = null
|
||||
val defaultHash = "LEHV6nWB2yk8pyoJadR*.7kCMdnj"
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Drawable>) {
|
||||
job = scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
val result = runCatching {
|
||||
(
|
||||
tryOrNull { BlurHashDecoder.decode(hash ?: defaultHash, width, height) }
|
||||
?: createImage(width, height, ThemeUtils.getColor(context, R.attr.riotx_reaction_background_off))
|
||||
)
|
||||
?.let {
|
||||
BitmapDrawable(
|
||||
context.resources,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.fold(
|
||||
{ callback.onDataReady(it) },
|
||||
{ callback.onLoadFailed(Exception(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
// ?
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
// nop?
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
override fun getDataClass(): Class<Drawable> {
|
||||
return Drawable::class.java
|
||||
}
|
||||
|
||||
fun createImage(width: Int, height: Int, color: Int): Bitmap? {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint()
|
||||
paint.setColor(color)
|
||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
|
||||
return bitmap
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.app.core.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -35,6 +36,12 @@ class MyAppGlideModule : AppGlideModule() {
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.append(
|
||||
BlurHashData::class.java,
|
||||
Drawable::class.java,
|
||||
BlurHashModelLoaderFactory(context)
|
||||
)
|
||||
|
||||
registry.append(
|
||||
ImageContentRenderer.Data::class.java,
|
||||
InputStream::class.java,
|
||||
|
@@ -32,11 +32,15 @@ import im.vector.app.features.session.coroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Will be used to download encrypted attachment.
|
||||
* Clear attachment will use regular http URL fetcher
|
||||
*/
|
||||
class VectorGlideModelLoaderFactory(private val context: Context) : ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||
@@ -69,7 +73,7 @@ class VectorGlideDataFetcher(context: Context,
|
||||
private val localFilesHelper = LocalFilesHelper(context)
|
||||
private val activeSessionHolder = context.vectorComponent().activeSessionHolder()
|
||||
|
||||
private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
|
||||
// private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
@@ -102,14 +106,14 @@ class VectorGlideDataFetcher(context: Context,
|
||||
}
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
Timber.v("Load data: $data")
|
||||
Timber.v("## Load data: ${data.url} is Local ${localFilesHelper.isLocalFile(data.url)}")
|
||||
if (localFilesHelper.isLocalFile(data.url)) {
|
||||
localFilesHelper.openInputStream(data.url)?.use {
|
||||
Timber.v("## Load data: Got a local input stream!!")
|
||||
callback.onDataReady(it)
|
||||
}
|
||||
return
|
||||
}
|
||||
// val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
|
||||
val fileService = activeSessionHolder.getSafeActiveSession()?.fileService() ?: return Unit.also {
|
||||
callback.onLoadFailed(IllegalArgumentException("No File service"))
|
||||
@@ -125,30 +129,16 @@ class VectorGlideDataFetcher(context: Context,
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.fold(
|
||||
{ callback.onDataReady(it.inputStream()) },
|
||||
{
|
||||
callback.onDataReady(
|
||||
// from crash report (clearing eg clear media cache while download completing?)
|
||||
tryOrNull {
|
||||
it.inputStream()
|
||||
})
|
||||
},
|
||||
{ callback.onLoadFailed(it as? Exception ?: IOException(it.localizedMessage)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
// 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()) {
|
||||
// Matrix.decryptStream(inputStream, data.elementToDecrypt)
|
||||
// } else {
|
||||
// inputStream
|
||||
// }
|
||||
// callback.onDataReady(stream)
|
||||
}
|
||||
}
|
||||
|
99
vector/src/main/java/im/vector/app/core/platform/TimelineMediaStateView.kt
Executable file
99
vector/src/main/java/im/vector/app/core/platform/TimelineMediaStateView.kt
Executable file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
import im.vector.app.databinding.MediaViewStateBinding
|
||||
|
||||
class TimelineMediaStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
||||
: FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
sealed class State {
|
||||
object NotDownloaded : State()
|
||||
data class Downloading(val progress: Int, val indeterminate: Boolean) : State()
|
||||
object PermanentError : State()
|
||||
object ReadyToPlay : State()
|
||||
object None : State()
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
interface Callback {
|
||||
fun onButtonClicked()
|
||||
}
|
||||
|
||||
private val views: MediaViewStateBinding
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.media_view_state, this)
|
||||
views = MediaViewStateBinding.bind(this)
|
||||
|
||||
layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
// views.buttonStateRetry.setOnClickListener {
|
||||
// callback?.onRetryClicked()
|
||||
// }
|
||||
|
||||
// Read attributes
|
||||
// context.theme.obtainStyledAttributes(
|
||||
// attrs,
|
||||
// R.styleable.ButtonStateView,
|
||||
// 0, 0)
|
||||
// .apply {
|
||||
// try {
|
||||
// if (getBoolean(R.styleable.ButtonStateView_bsv_use_flat_button, true)) {
|
||||
// button = views.buttonStateButtonFlat
|
||||
// views.buttonStateButtonBig.isVisible = false
|
||||
// } else {
|
||||
// button = views.buttonStateButtonBig
|
||||
// views.buttonStateButtonFlat.isVisible = false
|
||||
// }
|
||||
//
|
||||
// button.text = getString(R.styleable.ButtonStateView_bsv_button_text)
|
||||
// views.buttonStateLoaded.setImageDrawable(getDrawable(R.styleable.ButtonStateView_bsv_loaded_image_src))
|
||||
// } finally {
|
||||
// recycle()
|
||||
// }
|
||||
// }
|
||||
|
||||
if (isInEditMode) {
|
||||
render(State.Downloading(20, false))
|
||||
}
|
||||
|
||||
setOnClickListener(DebouncedClickListener({
|
||||
callback?.onButtonClicked()
|
||||
}))
|
||||
}
|
||||
|
||||
fun render(newState: State) {
|
||||
isVisible = newState != State.None
|
||||
views.mediaStateNotDownloaded.isVisible = newState == State.NotDownloaded
|
||||
views.circularProgress.isVisible = newState is State.Downloading
|
||||
(newState as? State.Downloading)?.let {
|
||||
views.mediaProgressView.progressBar.progress = it.progress
|
||||
views.mediaProgressView.progressBar.isIndeterminate = it.indeterminate
|
||||
}
|
||||
views.mediaStateError.isVisible = newState == State.PermanentError
|
||||
views.mediaStatePlay.isVisible = newState == State.ReadyToPlay
|
||||
}
|
||||
}
|
@@ -36,3 +36,7 @@ class DebouncedClickListener(val original: View.OnClickListener, private val min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setDebouncedClickListener(clickListener: View.OnClickListener?) {
|
||||
setOnClickListener(clickListener?.let { DebouncedClickListener(it) })
|
||||
}
|
||||
|
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import javax.inject.Inject
|
||||
|
||||
class SizeByteFormatter @Inject constructor(private val context: Context) {
|
||||
fun formatFileSize(sizeBytes: Long, useShortFormat: Boolean = false): String {
|
||||
return TextUtils.formatFileSize(context, sizeBytes, useShortFormat)
|
||||
}
|
||||
}
|
@@ -64,6 +64,7 @@ import im.vector.app.features.html.SpanUtils
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import me.gujun.android.span.span
|
||||
import org.commonmark.node.Document
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@@ -111,6 +112,7 @@ class MessageItemFactory @Inject constructor(
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val spanUtils: SpanUtils,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val session: Session) {
|
||||
|
||||
// TODO inject this properly?
|
||||
@@ -120,6 +122,24 @@ class MessageItemFactory @Inject constructor(
|
||||
pillsPostProcessorFactory.create(roomId)
|
||||
}
|
||||
|
||||
private val shouldAutoPlayGif: Boolean by lazy {
|
||||
val autoPref = vectorPreferences.getGifAutoPlayPreference()
|
||||
session.getRoomSummary(roomId)?.let {
|
||||
if (it.isDirect) {
|
||||
autoPref == VectorPreferences.GifAutoPlayPreference.ALWAYS
|
||||
|| autoPref == VectorPreferences.GifAutoPlayPreference.DM
|
||||
} else {
|
||||
if (it.isPublic) {
|
||||
autoPref == VectorPreferences.GifAutoPlayPreference.ALWAYS
|
||||
} else {
|
||||
autoPref == VectorPreferences.GifAutoPlayPreference.ALWAYS
|
||||
|| autoPref == VectorPreferences.GifAutoPlayPreference.DM_PRIVATE
|
||||
}
|
||||
}
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||
val event = params.event
|
||||
val highlight = params.isHighlighted
|
||||
@@ -308,13 +328,24 @@ class MessageItemFactory @Inject constructor(
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
maxWidth = maxWidth,
|
||||
allowNonMxcUrls = informationData.sendState.isSending()
|
||||
allowNonMxcUrls = informationData.sendState.isSending(),
|
||||
blurHash = messageContent.info?.blurHash
|
||||
)
|
||||
return MessageImageVideoItem_()
|
||||
.attributes(attributes)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.imageContentRenderer(imageContentRenderer)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
.dimensionConverter(dimensionConverter)
|
||||
.autoPlayGifs(shouldAutoPlayGif)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
messageContent.getFileUrl(),
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.playable(messageContent.info?.mimeType == MimeTypes.Gif)
|
||||
.highlighted(highlight)
|
||||
.mediaData(data)
|
||||
@@ -346,7 +377,8 @@ class MessageItemFactory @Inject constructor(
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.videoInfo?.width,
|
||||
maxWidth = maxWidth,
|
||||
allowNonMxcUrls = informationData.sendState.isSending()
|
||||
allowNonMxcUrls = informationData.sendState.isSending(),
|
||||
blurHash = messageContent.videoInfo?.blurHash
|
||||
)
|
||||
|
||||
val videoData = VideoContentRenderer.Data(
|
||||
@@ -363,6 +395,15 @@ class MessageItemFactory @Inject constructor(
|
||||
.attributes(attributes)
|
||||
.imageContentRenderer(imageContentRenderer)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
.dimensionConverter(dimensionConverter)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
messageContent.getFileUrl(),
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.playable(true)
|
||||
.highlighted(highlight)
|
||||
.mediaData(thumbnailData)
|
||||
|
@@ -23,8 +23,10 @@ import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.ScreenScope
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.platform.TimelineMediaStateView
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -33,13 +35,24 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) {
|
||||
|
||||
private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>()
|
||||
private val updateListeners = mutableMapOf<String, DownloadUpdater>()
|
||||
|
||||
fun bind(mxcUrl: String,
|
||||
holder: MessageFileItem.Holder) {
|
||||
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
||||
val downloadStateTracker = session.contentDownloadProgressTracker()
|
||||
val updateListener = ContentDownloadUpdater(holder, messageColorProvider, errorFormatter)
|
||||
val updateListener = FileContentDownloadUpdater(holder)
|
||||
updateListeners[mxcUrl] = updateListener
|
||||
downloadStateTracker.track(mxcUrl, updateListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(mxcUrl: String,
|
||||
playable: Boolean,
|
||||
holder: MessageImageVideoItem.Holder) {
|
||||
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
||||
val downloadStateTracker = session.contentDownloadProgressTracker()
|
||||
val updateListener = ImageContentDownloadUpdater(holder, playable)
|
||||
updateListeners[mxcUrl] = updateListener
|
||||
downloadStateTracker.track(mxcUrl, updateListener)
|
||||
}
|
||||
@@ -52,6 +65,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
||||
it.stop()
|
||||
downloadStateTracker.unTrack(mxcUrl, it)
|
||||
}
|
||||
updateListeners.remove(mxcUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +76,11 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
||||
}
|
||||
}
|
||||
|
||||
private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) : ContentDownloadStateTracker.UpdateListener {
|
||||
private interface DownloadUpdater: ContentDownloadStateTracker.UpdateListener {
|
||||
fun stop()
|
||||
}
|
||||
|
||||
private class FileContentDownloadUpdater(private val holder: MessageFileItem.Holder) : DownloadUpdater {
|
||||
|
||||
override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) {
|
||||
when (state) {
|
||||
@@ -83,7 +99,7 @@ private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
override fun stop() {
|
||||
animatedDrawable?.unregisterAnimationCallback(animationLoopCallback)
|
||||
animatedDrawable?.stop()
|
||||
animatedDrawable = null
|
||||
@@ -128,3 +144,62 @@ private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
||||
holder.fileImageView.setImageResource(R.drawable.ic_paperclip)
|
||||
}
|
||||
}
|
||||
|
||||
private class ImageContentDownloadUpdater(private val holder: MessageImageVideoItem.Holder, val playable: Boolean) : DownloadUpdater {
|
||||
|
||||
override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) {
|
||||
when (state) {
|
||||
ContentDownloadStateTracker.State.Idle -> handleIdle()
|
||||
is ContentDownloadStateTracker.State.Downloading -> handleProgress(state)
|
||||
ContentDownloadStateTracker.State.Decrypting -> handleDecrypting()
|
||||
ContentDownloadStateTracker.State.Success -> handleSuccess()
|
||||
is ContentDownloadStateTracker.State.Failure -> handleFailure()
|
||||
}
|
||||
}
|
||||
|
||||
// private var animatedDrawable: AnimatedVectorDrawableCompat? = null
|
||||
// private var animationLoopCallback = object : Animatable2Compat.AnimationCallback() {
|
||||
// override fun onAnimationEnd(drawable: Drawable?) {
|
||||
// animatedDrawable?.start()
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun stop() {
|
||||
// animatedDrawable?.unregisterAnimationCallback(animationLoopCallback)
|
||||
// animatedDrawable?.stop()
|
||||
// animatedDrawable = null
|
||||
}
|
||||
|
||||
private fun handleIdle() {
|
||||
holder.mediaStateView.render(TimelineMediaStateView.State.Downloading(0, true))
|
||||
}
|
||||
|
||||
private fun handleDecrypting() {
|
||||
holder.mediaStateView.render(TimelineMediaStateView.State.Downloading(0, true))
|
||||
}
|
||||
|
||||
private fun handleProgress(state: ContentDownloadStateTracker.State.Downloading) {
|
||||
doHandleProgress(state.current, state.total)
|
||||
}
|
||||
|
||||
private fun doHandleProgress(current: Long, total: Long) {
|
||||
val percent = 100L * (current.toFloat() / total.toFloat())
|
||||
holder.mediaStateView.render(TimelineMediaStateView.State.Downloading(percent.toInt(), false))
|
||||
// if (animatedDrawable == null) {
|
||||
// animatedDrawable = AnimatedVectorDrawableCompat.create(holder.view.context, R.drawable.ic_download_anim)
|
||||
// holder.fileImageView.setImageDrawable(animatedDrawable)
|
||||
// animatedDrawable?.start()
|
||||
// animatedDrawable?.registerAnimationCallback(animationLoopCallback)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun handleFailure() {
|
||||
stop()
|
||||
holder.mediaStateView.render(TimelineMediaStateView.State.PermanentError)
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
stop()
|
||||
holder.mediaStateView.render(if (playable) TimelineMediaStateView.State.ReadyToPlay else TimelineMediaStateView.State.None)
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.ScreenScope
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.TimelineMediaStateView
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
|
||||
@@ -42,10 +43,11 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
|
||||
|
||||
fun bind(eventId: String,
|
||||
isLocalFile: Boolean,
|
||||
progressLayout: ViewGroup) {
|
||||
progressLayout: ViewGroup,
|
||||
mediaStateView: TimelineMediaStateView?) {
|
||||
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
||||
val uploadStateTracker = session.contentUploadProgressTracker()
|
||||
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, messageColorProvider, errorFormatter)
|
||||
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaStateView, isLocalFile, messageColorProvider, errorFormatter)
|
||||
updateListeners[eventId] = updateListener
|
||||
uploadStateTracker.track(eventId, updateListener)
|
||||
}
|
||||
@@ -68,6 +70,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
|
||||
}
|
||||
|
||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
private val mediaStateView: TimelineMediaStateView?,
|
||||
private val isLocalFile: Boolean,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener {
|
||||
@@ -91,14 +94,20 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
|
||||
private fun handleIdle() {
|
||||
if (isLocalFile) {
|
||||
if (mediaStateView != null) {
|
||||
progressBar.isVisible = false
|
||||
mediaStateView.render(TimelineMediaStateView.State.Downloading(0, true))
|
||||
} else {
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = true
|
||||
progressBar.progress = 0
|
||||
}
|
||||
progressLayout.isVisible = true
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = true
|
||||
progressBar.progress = 0
|
||||
progressTextView.text = progressLayout.context.getString(R.string.send_file_step_idle)
|
||||
progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.UNSENT))
|
||||
} else {
|
||||
progressLayout.isVisible = false
|
||||
mediaStateView?.render(TimelineMediaStateView.State.Downloading(0, true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +129,13 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
|
||||
private fun handleCompressingImage() {
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = true
|
||||
if (mediaStateView != null) {
|
||||
progressBar.isVisible = false
|
||||
mediaStateView.render(TimelineMediaStateView.State.Downloading(10, true))
|
||||
} else {
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = true
|
||||
}
|
||||
progressTextView.isVisible = true
|
||||
progressTextView.text = progressLayout.context.getString(R.string.send_file_step_compressing_image)
|
||||
progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING))
|
||||
@@ -131,9 +145,14 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun handleCompressingVideo(state: ContentUploadStateTracker.State.CompressingVideo) {
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = state.percent.toInt()
|
||||
if (mediaStateView != null) {
|
||||
progressBar.isVisible = false
|
||||
mediaStateView.render(TimelineMediaStateView.State.Downloading(state.percent.toInt(), false))
|
||||
} else {
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = state.percent.toInt()
|
||||
}
|
||||
progressTextView.isVisible = true
|
||||
// False positive is here...
|
||||
progressTextView.text = progressLayout.context.getString(R.string.send_file_step_compressing_video, state.percent.toInt())
|
||||
@@ -141,21 +160,33 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
}
|
||||
|
||||
private fun doHandleEncrypting(resId: Int, current: Long, total: Long) {
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
val percent = if (total > 0) (100L * (current.toFloat() / total.toFloat())) else 0f
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = percent.toInt()
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
if (mediaStateView != null) {
|
||||
progressBar.isVisible = false
|
||||
mediaStateView.render(TimelineMediaStateView.State.Downloading(percent.toInt(), false))
|
||||
} else {
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = percent.toInt()
|
||||
}
|
||||
|
||||
progressTextView.isVisible = true
|
||||
progressTextView.text = progressLayout.context.getString(resId)
|
||||
progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.ENCRYPTING))
|
||||
}
|
||||
|
||||
private fun doHandleProgress(resId: Int, current: Long, total: Long) {
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
val percent = 100L * (current.toFloat() / total.toFloat())
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = percent.toInt()
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
if (mediaStateView != null) {
|
||||
progressBar.isVisible = false
|
||||
mediaStateView.render(TimelineMediaStateView.State.Downloading(percent.toInt(), false))
|
||||
} else {
|
||||
progressBar.isVisible = true
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = percent.toInt()
|
||||
}
|
||||
progressTextView.isVisible = true
|
||||
progressTextView.text = progressLayout.context.getString(resId,
|
||||
TextUtils.formatFileSize(progressLayout.context, current, true),
|
||||
@@ -164,6 +195,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
}
|
||||
|
||||
private fun handleFailure(/*state: ContentUploadStateTracker.State.Failure*/) {
|
||||
mediaStateView?.render(TimelineMediaStateView.State.PermanentError)
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
progressBar.isVisible = false
|
||||
// Do not show the message it's too technical for users, and unfortunate when upload is cancelled
|
||||
|
@@ -41,7 +41,8 @@ fun TimelineEvent.buildImageContentRendererData(maxHeight: Int): ImageContentRen
|
||||
maxHeight = maxHeight,
|
||||
width = messageImageContent.info?.width,
|
||||
maxWidth = maxHeight * 2,
|
||||
allowNonMxcUrls = false
|
||||
allowNonMxcUrls = false,
|
||||
blurHash = messageImageContent.info?.blurHash
|
||||
)
|
||||
}
|
||||
root.isVideoMessage() -> root.getClearContent().toModel<MessageVideoContent>()
|
||||
@@ -57,7 +58,8 @@ fun TimelineEvent.buildImageContentRendererData(maxHeight: Int): ImageContentRen
|
||||
maxHeight = maxHeight,
|
||||
width = videoInfo?.thumbnailInfo?.width,
|
||||
maxWidth = maxHeight * 2,
|
||||
allowNonMxcUrls = false
|
||||
allowNonMxcUrls = false,
|
||||
blurHash = videoInfo?.blurHash
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
|
@@ -61,7 +61,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.fileLayout, holder.filenameView)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout, null)
|
||||
} else {
|
||||
holder.fileImageView.setImageResource(R.drawable.ic_cross)
|
||||
holder.progressLayout.isVisible = false
|
||||
|
@@ -26,6 +26,9 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.files.LocalFilesHelper
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.platform.TimelineMediaStateView
|
||||
import im.vector.app.core.utils.setDebouncedClickListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
|
||||
@@ -50,30 +53,90 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute
|
||||
var izLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var izDownloaded = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var autoPlayGifs = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
imageContentRenderer.render(mediaData, mode, holder.imageView)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(
|
||||
attributes.informationData.eventId,
|
||||
LocalFilesHelper(holder.view.context).isLocalFile(mediaData.url),
|
||||
holder.progressLayout
|
||||
holder.progressLayout,
|
||||
holder.mediaStateView
|
||||
)
|
||||
} else {
|
||||
holder.progressLayout.isVisible = false
|
||||
}
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
|
||||
if (!izDownloaded && !izLocalFile) {
|
||||
contentDownloadStateTrackerBinder.bind(
|
||||
mediaData.url ?: "",
|
||||
playable && !autoPlayGifs,
|
||||
holder)
|
||||
} else {
|
||||
holder.mediaStateView.render(if (playable && !autoPlayGifs) TimelineMediaStateView.State.ReadyToPlay else TimelineMediaStateView.State.None)
|
||||
}
|
||||
holder.mediaStateView.setTag(R.id.messageMediaStateView, mediaData.url)
|
||||
imageContentRenderer.render(mediaData, mode, holder.imageView, autoPlayGifs, rendererCallbacks = object: ImageContentRenderer.ContentRendererCallbacks {
|
||||
override fun onThumbnailModeFinish(success: Boolean) {
|
||||
// if a server thumbnail was used the download tracker won't be called
|
||||
mediaData.url?.let { mxcUrl ->
|
||||
if (mxcUrl == holder.mediaStateView.getTag(R.id.messageMediaStateView)) {
|
||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
// mmm annoying but have to post if not the previous contentDownloadStateTrackerBinder.bind call we render
|
||||
// an IDLE state (i.e a loading wheel...)
|
||||
holder.mediaStateView.post {
|
||||
holder.mediaStateView.render(if (success) TimelineMediaStateView.State.None else TimelineMediaStateView.State.PermanentError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadModeFinish(success: Boolean) {
|
||||
// if a server thumbnail was used the download tracker won't be called
|
||||
mediaData.url?.let { mxcUrl ->
|
||||
if (mxcUrl == holder.mediaStateView.getTag(R.id.messageMediaStateView)) {
|
||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
// mmm annoying but have to post if not the previous contentDownloadStateTrackerBinder.bind call we render
|
||||
// an IDLE state (i.e a loading wheel...)
|
||||
holder.mediaStateView.post {
|
||||
holder.mediaStateView.render(if (success) TimelineMediaStateView.State.None else TimelineMediaStateView.State.PermanentError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
holder.mediaStateView.callback = object : TimelineMediaStateView.Callback {
|
||||
override fun onButtonClicked() {
|
||||
// for now delegate to regular click
|
||||
clickListener?.onClick(holder.imageView)
|
||||
}
|
||||
}
|
||||
holder.imageView.setDebouncedClickListener(clickListener)
|
||||
holder.imageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
|
||||
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
|
||||
holder.mediaContentView.setDebouncedClickListener(attributes.itemClickListener)
|
||||
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||
// holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.mediaStateView.setTag(R.id.messageMediaStateView, null)
|
||||
GlideApp.with(holder.view.context.applicationContext).clear(holder.imageView)
|
||||
imageContentRenderer.clear(holder.imageView)
|
||||
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
|
||||
contentDownloadStateTrackerBinder.unbind(mediaData.url ?: "")
|
||||
holder.imageView.setOnClickListener(null)
|
||||
holder.imageView.setOnLongClickListener(null)
|
||||
super.unbind(holder)
|
||||
@@ -83,8 +146,10 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||
val mediaStateView by bind<TimelineMediaStateView>(R.id.messageMediaStateView)
|
||||
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||
|
||||
// val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.media
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
@@ -28,22 +29,27 @@ import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.target.ImageViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.files.LocalFilesHelper
|
||||
import im.vector.app.core.glide.BlurHashData
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequest
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.ui.model.Size
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import timber.log.Timber
|
||||
import xyz.belvi.blurhash.BlurHash
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.min
|
||||
@@ -61,7 +67,9 @@ interface AttachmentData : Parcelable {
|
||||
|
||||
class ImageContentRenderer @Inject constructor(private val localFilesHelper: LocalFilesHelper,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val dimensionConverter: DimensionConverter) {
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
// private val contentDownloadStateTracker: ContentDownloadStateTracker,
|
||||
private val blurHash: BlurHash) {
|
||||
|
||||
@Parcelize
|
||||
data class Data(
|
||||
@@ -74,8 +82,10 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
val maxHeight: Int,
|
||||
val width: Int?,
|
||||
val maxWidth: Int,
|
||||
val blurHash: String?,
|
||||
// If true will load non mxc url, be careful to set it only for images sent by you
|
||||
override val allowNonMxcUrls: Boolean = false
|
||||
override val allowNonMxcUrls: Boolean = false,
|
||||
val autoDownload: Boolean = false
|
||||
) : AttachmentData
|
||||
|
||||
enum class Mode {
|
||||
@@ -109,8 +119,20 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView) {
|
||||
interface ContentRendererCallbacks {
|
||||
fun onThumbnailModeFinish(success: Boolean)
|
||||
|
||||
fun onLoadModeFinish(success: Boolean)
|
||||
}
|
||||
/**
|
||||
* In timeline
|
||||
* All encrypted media will be downloaded by the SDK's FileService, so caller could follow progress using download tracker,
|
||||
* but for clear media a server thumbnail will be requested and in this case it will be invisible to download tracker that's why there is the
|
||||
* `mxcThumbnailCallback` callback. Caller can use it to know when media is loaded.
|
||||
*/
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView, animate: Boolean = false, rendererCallbacks: ContentRendererCallbacks? = null) {
|
||||
val size = processSize(data, mode)
|
||||
// This size will be used by glide for bitmap size
|
||||
imageView.updateLayoutParams {
|
||||
width = size.width
|
||||
height = size.height
|
||||
@@ -118,11 +140,47 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
// a11y
|
||||
imageView.contentDescription = data.filename
|
||||
|
||||
createGlideRequest(data, mode, imageView, size)
|
||||
.dontAnimate()
|
||||
createGlideRequest(data, mode, imageView, size, animate, rendererCallbacks)
|
||||
.apply {
|
||||
if (!animate) {
|
||||
dontAnimate()
|
||||
}
|
||||
}
|
||||
// .dontAnimate()
|
||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
||||
// .thumbnail(0.3f)
|
||||
.into(imageView)
|
||||
.placeholder(ColorDrawable(ThemeUtils.getColor(imageView.context, R.attr.riotx_reaction_background_off)))
|
||||
.apply {
|
||||
if (data.blurHash != null) {
|
||||
thumbnail(
|
||||
GlideApp.with(imageView)
|
||||
.load(BlurHashData(data.blurHash))
|
||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
.apply {
|
||||
// In case of permanent error, the thumbnail might not be loaded and images goes directly to blank state instead of
|
||||
// loading the blurhash thumbnail.. so ensure that error will use the blur hash
|
||||
if (data.blurHash != null) {
|
||||
error(
|
||||
GlideApp.with(imageView)
|
||||
.load(BlurHashData(data.blurHash))
|
||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
).error(ColorDrawable(ThemeUtils.getColor(imageView.context, R.attr.riotx_reaction_background_off)))
|
||||
} else {
|
||||
error(ColorDrawable(ThemeUtils.getColor(imageView.context, R.attr.riotx_reaction_background_off)))
|
||||
}
|
||||
}
|
||||
.into(object : ImageViewTarget<Drawable>(imageView) {
|
||||
override fun setResource(resource: Drawable?) {
|
||||
resource?.let {
|
||||
imageView.post { imageView.setImageDrawable(it) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun clear(imageView: ImageView) {
|
||||
@@ -150,41 +208,46 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
GlideApp
|
||||
.with(contextView)
|
||||
.load(resolvedUrl)
|
||||
.thumbnail()
|
||||
}
|
||||
|
||||
req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
|
||||
.thumbnail(
|
||||
GlideApp.with(contextView)
|
||||
.load(BlurHashData(data.blurHash))
|
||||
)
|
||||
.fitCenter()
|
||||
.into(target)
|
||||
}
|
||||
|
||||
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
|
||||
val size = processSize(data, mode)
|
||||
|
||||
// a11y
|
||||
imageView.contentDescription = data.filename
|
||||
|
||||
createGlideRequest(data, mode, imageView, size)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean): Boolean {
|
||||
callback?.invoke(false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean): Boolean {
|
||||
callback?.invoke(true)
|
||||
return false
|
||||
}
|
||||
})
|
||||
.fitCenter()
|
||||
.into(imageView)
|
||||
}
|
||||
// fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
|
||||
// val size = processSize(data, mode)
|
||||
//
|
||||
// // a11y
|
||||
// imageView.contentDescription = data.filename
|
||||
//
|
||||
// createGlideRequest(data, mode, imageView, size)
|
||||
// .listener(object : RequestListener<Drawable> {
|
||||
// override fun onLoadFailed(e: GlideException?,
|
||||
// model: Any?,
|
||||
// target: Target<Drawable>?,
|
||||
// isFirstResource: Boolean): Boolean {
|
||||
// callback?.invoke(false)
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// override fun onResourceReady(resource: Drawable?,
|
||||
// model: Any?,
|
||||
// target: Target<Drawable>?,
|
||||
// dataSource: DataSource?,
|
||||
// isFirstResource: Boolean): Boolean {
|
||||
// callback?.invoke(true)
|
||||
// return false
|
||||
// }
|
||||
// })
|
||||
// .fitCenter()
|
||||
// .into(imageView)
|
||||
// }
|
||||
|
||||
/**
|
||||
* onlyRetrieveFromCache is true!
|
||||
@@ -197,13 +260,14 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.asDrawable()
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
// Clear image
|
||||
val resolvedUrl = resolveUrl(data)
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.asDrawable()
|
||||
.load(resolvedUrl)
|
||||
}
|
||||
|
||||
@@ -230,36 +294,124 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size, autoplay: Boolean = false, rendererCallbacks: ContentRendererCallbacks? = null): GlideRequest<Drawable> {
|
||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size, autoplay, rendererCallbacks)
|
||||
}
|
||||
|
||||
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), autoplay: Boolean = false, rendererCallbacks: ContentRendererCallbacks? = null): GlideRequest<Drawable> {
|
||||
return if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
glideRequests
|
||||
.apply {
|
||||
if (!autoplay && data.mimeType == MimeTypes.Gif) {
|
||||
// if it's a gif and that we don't auto play,
|
||||
// there is no point of loading all frames, just use this to take first one
|
||||
asBitmap()
|
||||
}
|
||||
}
|
||||
.load(data)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onLoadModeFinish(false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onLoadModeFinish(true)
|
||||
return false
|
||||
}
|
||||
})
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE,
|
||||
Mode.STICKER -> resolveUrl(data)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
// Fallback to base url
|
||||
?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
// Check if it's worth it asking the server for a thumbnail
|
||||
val shouldQueryThumb = if (mode == Mode.THUMBNAIL) {
|
||||
(data.width == null || data.height == null
|
||||
|| size.width * size.height < (data.width * data.height) * 2)
|
||||
} else false
|
||||
|
||||
glideRequests
|
||||
.load(resolvedUrl)
|
||||
.apply {
|
||||
if (mode == Mode.THUMBNAIL) {
|
||||
error(
|
||||
glideRequests.load(resolveUrl(data))
|
||||
)
|
||||
if (shouldQueryThumb) {
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
glideRequests
|
||||
.apply {
|
||||
if (!autoplay && data.mimeType == MimeTypes.Gif) {
|
||||
// if it's a gif and that we don't auto play,
|
||||
// there is no point of loading all frames, just use this to take first one
|
||||
asBitmap()
|
||||
}
|
||||
}.load(
|
||||
contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
).listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onThumbnailModeFinish(false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onThumbnailModeFinish(true)
|
||||
return false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
glideRequests
|
||||
.apply {
|
||||
if (!autoplay && data.mimeType == MimeTypes.Gif) {
|
||||
// if it's a gif and that we don't auto play,
|
||||
// there is no point of loading all frames, just use this to take first one
|
||||
asBitmap()
|
||||
}
|
||||
}
|
||||
}
|
||||
.load(data)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onLoadModeFinish(false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
rendererCallbacks?.onLoadModeFinish(true)
|
||||
return false
|
||||
}
|
||||
})
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
}
|
||||
|
||||
// glideRequests
|
||||
// .apply {
|
||||
// if (!autoplay && data.mimeType == MimeTypes.Gif) {
|
||||
// // if it's a gif and that we don't auto play,
|
||||
// // there is no point of loading all frames, just use this to take first one
|
||||
// asBitmap()
|
||||
// }
|
||||
// }
|
||||
// .apply {
|
||||
// if (shouldQueryThumb) {
|
||||
// load(
|
||||
// contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
// ?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
// )
|
||||
// } else {
|
||||
// load(data).diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
// }
|
||||
// }
|
||||
// .load(
|
||||
// if (shouldQueryThumb) {
|
||||
// contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
// ?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
// } else {
|
||||
// data
|
||||
// }
|
||||
// )
|
||||
// cache is handled by the VectorGlideModelLoader
|
||||
// .diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
// .apply {
|
||||
// if (mode == Mode.THUMBNAIL) {
|
||||
// error(
|
||||
// glideRequests.load(resolveUrl(data))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -64,8 +64,8 @@ class RoomEventsAttachmentProvider(
|
||||
maxWidth = -1,
|
||||
width = null,
|
||||
height = null,
|
||||
allowNonMxcUrls = it.root.sendState.isSending()
|
||||
|
||||
allowNonMxcUrls = it.root.sendState.isSending(),
|
||||
blurHash = content.info?.blurHash
|
||||
)
|
||||
if (content.mimeType == MimeTypes.Gif) {
|
||||
AttachmentInfo.AnimatedImage(
|
||||
@@ -91,7 +91,8 @@ class RoomEventsAttachmentProvider(
|
||||
maxHeight = -1,
|
||||
width = content.videoInfo?.width,
|
||||
maxWidth = -1,
|
||||
allowNonMxcUrls = it.root.sendState.isSending()
|
||||
allowNonMxcUrls = it.root.sendState.isSending(),
|
||||
blurHash = content.videoInfo?.blurHash
|
||||
)
|
||||
val data = VideoContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
|
@@ -141,6 +141,15 @@ class VectorFileLogger @Inject constructor(
|
||||
logger.info(errors.toString())
|
||||
}
|
||||
|
||||
fun getLogSize(): Int {
|
||||
return cacheDirectory.walkTopDown()
|
||||
.onEnter {
|
||||
Timber.v("Get size of ${it.absolutePath}")
|
||||
true
|
||||
}
|
||||
.sumOf { it.length().toInt() }
|
||||
}
|
||||
|
||||
private fun logToFile(level: String, tag: String, content: String) {
|
||||
val b = StringBuilder()
|
||||
b.append(Thread.currentThread().id)
|
||||
|
@@ -134,7 +134,8 @@ class RoomUploadsMediaFragment @Inject constructor(
|
||||
maxHeight = -1,
|
||||
maxWidth = -1,
|
||||
width = null,
|
||||
height = null
|
||||
height = null,
|
||||
blurHash = content.info?.blurHash
|
||||
)
|
||||
}
|
||||
is MessageVideoContent -> {
|
||||
@@ -147,7 +148,8 @@ class RoomUploadsMediaFragment @Inject constructor(
|
||||
height = content.videoInfo?.height,
|
||||
maxHeight = -1,
|
||||
width = content.videoInfo?.width,
|
||||
maxWidth = -1
|
||||
maxWidth = -1,
|
||||
blurHash = content.videoInfo?.blurHash
|
||||
)
|
||||
VideoContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
|
@@ -123,7 +123,8 @@ class UploadsMediaController @Inject constructor(
|
||||
height = messageContent.info?.height,
|
||||
maxHeight = itemSize,
|
||||
width = messageContent.info?.width,
|
||||
maxWidth = itemSize
|
||||
maxWidth = itemSize,
|
||||
blurHash = messageContent.info?.blurHash
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,7 +140,8 @@ class UploadsMediaController @Inject constructor(
|
||||
height = messageContent.videoInfo?.height,
|
||||
maxHeight = itemSize,
|
||||
width = messageContent.videoInfo?.width,
|
||||
maxWidth = itemSize
|
||||
maxWidth = itemSize,
|
||||
blurHash = messageContent.videoInfo?.blurHash
|
||||
)
|
||||
|
||||
return VideoContentRenderer.Data(
|
||||
|
@@ -33,6 +33,13 @@ import javax.inject.Inject
|
||||
|
||||
class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
|
||||
enum class GifAutoPlayPreference(val value:String) {
|
||||
NEVER("never"),
|
||||
DM("dm"),
|
||||
DM_PRIVATE("dm_private"),
|
||||
ALWAYS("always")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY"
|
||||
const val SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY"
|
||||
@@ -43,6 +50,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
|
||||
const val SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY"
|
||||
const val SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY"
|
||||
const val SETTINGS_STORAGE_USAGE_PREFERENCE_KEY = "SETTINGS_STORAGE_USAGE_PREFERENCE_KEY"
|
||||
|
||||
const val SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY"
|
||||
const val SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
|
||||
@@ -129,6 +137,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
private const val SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY"
|
||||
private const val SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY"
|
||||
private const val SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY"
|
||||
const val SETTINGS_AUTOPLAY_GIF_PREFERENCE = "SETTINGS_AUTOPLAY_GIF_PREFERENCE"
|
||||
|
||||
// background sync
|
||||
const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
|
||||
@@ -961,6 +970,15 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false)
|
||||
}
|
||||
|
||||
fun getGifAutoPlayPreference(): GifAutoPlayPreference {
|
||||
return when (defaultPrefs.getString(SETTINGS_AUTOPLAY_GIF_PREFERENCE, "never")) {
|
||||
"always" -> GifAutoPlayPreference.ALWAYS
|
||||
"dm" -> GifAutoPlayPreference.DM
|
||||
"dm_private" -> GifAutoPlayPreference.DM_PRIVATE
|
||||
else -> GifAutoPlayPreference.NEVER
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Photo / video picker
|
||||
*/
|
||||
|
@@ -50,6 +50,10 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||
findPreference<VectorPreference>("SETTINGS_INTERFACE_TAKE_PHOTO_VIDEO")!!
|
||||
}
|
||||
|
||||
private val autoPlayGifPreference by lazy {
|
||||
findPreference<VectorListPreference>(VectorPreferences.SETTINGS_AUTOPLAY_GIF_PREFERENCE)!!
|
||||
}
|
||||
|
||||
override fun bindPref() {
|
||||
// user interface preferences
|
||||
setUserInterfacePreferences()
|
||||
@@ -67,6 +71,11 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// set initial value
|
||||
findPreference<VectorListPreference>(VectorPreferences.SETTINGS_AUTOPLAY_GIF_PREFERENCE)?.let {
|
||||
it.value = vectorPreferences.getGifAutoPlayPreference().value
|
||||
}
|
||||
|
||||
// Url preview
|
||||
/*
|
||||
TODO Note: we keep the setting client side for now
|
||||
|
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.data
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.bumptech.glide.load.engine.cache.DiskCache
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||
import im.vector.app.features.rageshake.VectorFileLogger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
data class StorageUsageViewState(
|
||||
val sizeOfLogs: Async<Long> = Uninitialized,
|
||||
val sizeOfMediaAndFiles: Async<Long> = Uninitialized,
|
||||
val sizeOfCache: Async<Long> = Uninitialized,
|
||||
val sizeOfSessionDatabase: Async<Long> = Uninitialized,
|
||||
val sizeOfCryptoDatabase: Async<Long> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
sealed class StorageUsageViewEvents : VectorViewEvents
|
||||
|
||||
sealed class StorageUsageViewModelAction : VectorViewModelAction {
|
||||
object ClearMediaCache : StorageUsageViewModelAction()
|
||||
}
|
||||
|
||||
class StorageUsageViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: StorageUsageViewState,
|
||||
private val session: Session,
|
||||
private val vectorFileLogger: VectorFileLogger
|
||||
) : VectorViewModel<StorageUsageViewState, StorageUsageViewModelAction, StorageUsageViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val dlSize = session.fileService().getCacheSize().toLong()
|
||||
|
||||
setState {
|
||||
copy(
|
||||
sizeOfMediaAndFiles = Success(dlSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val logSize = vectorFileLogger.getLogSize().toLong()
|
||||
setState {
|
||||
copy(
|
||||
sizeOfLogs = Success(logSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val glideCacheSize = session.storageUsageService().cacheDirectorySize(DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)
|
||||
setState {
|
||||
copy(
|
||||
sizeOfCache = Success(glideCacheSize)
|
||||
)
|
||||
}
|
||||
|
||||
val cyptoSize = session.storageUsageService().cryptoDataBaseSize()
|
||||
val sessionSize = session.storageUsageService().sessionDataBaseSize()
|
||||
|
||||
setState {
|
||||
copy(
|
||||
sizeOfCryptoDatabase = Success(cyptoSize),
|
||||
sizeOfSessionDatabase = Success(sessionSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(initialState: StorageUsageViewState): StorageUsageViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<StorageUsageViewModel, StorageUsageViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: StorageUsageViewState): StorageUsageViewModel? {
|
||||
val factory = when (viewModelContext) {
|
||||
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||
}
|
||||
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: StorageUsageViewModelAction) {
|
||||
when (action) {
|
||||
StorageUsageViewModelAction.ClearMediaCache -> {
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
class DataAndStorageFragment @Inject constructor(
|
||||
val viewModelFactory: StorageUsageViewModel.Factory,
|
||||
private val epoxyController: StorageUsageController
|
||||
) : VectorBaseFragment<FragmentGenericRecyclerBinding>(), StorageUsageViewModel.Factory {
|
||||
|
||||
private val viewModel: StorageUsageViewModel by fragmentViewModel()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
|
||||
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun create(initialState: StorageUsageViewState) = viewModelFactory.create(initialState)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
views.genericRecyclerView.configureWith(epoxyController, showDivider = true)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.data
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.core.ui.list.genericItem
|
||||
import im.vector.app.core.utils.SizeByteFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
class StorageUsageController @Inject constructor(
|
||||
private val sizeByteFormatter: SizeByteFormatter
|
||||
) : TypedEpoxyController<StorageUsageViewState>() {
|
||||
|
||||
override fun buildModels(data: StorageUsageViewState?) {
|
||||
data ?: return
|
||||
|
||||
genericItem {
|
||||
id("media")
|
||||
title("Media and Files")
|
||||
description(
|
||||
data.sizeOfMediaAndFiles.invoke()?.let {
|
||||
this@StorageUsageController.sizeByteFormatter.formatFileSize(it)
|
||||
} ?: "--"
|
||||
)
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("logs")
|
||||
title("Log Files")
|
||||
description(
|
||||
data.sizeOfLogs.invoke()?.let {
|
||||
this@StorageUsageController.sizeByteFormatter.formatFileSize(it)
|
||||
} ?: "--"
|
||||
)
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("cache")
|
||||
title("Media cache")
|
||||
description(
|
||||
data.sizeOfCache.invoke()?.let {
|
||||
this@StorageUsageController.sizeByteFormatter.formatFileSize(it)
|
||||
} ?: "--"
|
||||
)
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("session")
|
||||
title("Session Database")
|
||||
description(
|
||||
data.sizeOfSessionDatabase.invoke()?.let {
|
||||
this@StorageUsageController.sizeByteFormatter.formatFileSize(it)
|
||||
} ?: "--"
|
||||
)
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("crypto_db")
|
||||
title("Crypto Database")
|
||||
description(
|
||||
data.sizeOfCryptoDatabase.invoke()?.let {
|
||||
this@StorageUsageController.sizeByteFormatter.formatFileSize(it)
|
||||
} ?: "--"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
23
vector/src/main/res/drawable/circular_progress_bar.xml
Normal file
23
vector/src/main/res/drawable/circular_progress_bar.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fromDegrees="270"
|
||||
android:toDegrees="270">
|
||||
|
||||
<!--styling the progress bar-->
|
||||
<shape
|
||||
android:innerRadiusRatio="2.5"
|
||||
android:shape="ring"
|
||||
android:thickness="3dp"
|
||||
android:useLevel="true">
|
||||
<solid android:color="@color/white"/>
|
||||
<!-- <gradient-->
|
||||
<!-- android:angle="0"-->
|
||||
<!-- android:endColor="@color/black"-->
|
||||
<!-- android:startColor="@color/white"-->
|
||||
<!-- android:type="sweep"-->
|
||||
<!-- android:useLevel="false" />-->
|
||||
</shape>
|
||||
|
||||
</rotate>
|
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fromDegrees="0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toDegrees="1080">
|
||||
<shape
|
||||
android:innerRadiusRatio="3"
|
||||
android:shape="ring"
|
||||
android:thicknessRatio="10"
|
||||
android:useLevel="false">
|
||||
|
||||
<gradient
|
||||
android:angle="0"
|
||||
android:endColor="#80FFFFFF"
|
||||
android:startColor="#00000000"
|
||||
android:type="sweep"
|
||||
android:useLevel="false" />
|
||||
</shape>
|
||||
</rotate>
|
9
vector/src/main/res/drawable/circular_progress_shape.xml
Normal file
9
vector/src/main/res/drawable/circular_progress_shape.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:innerRadiusRatio="2.5"
|
||||
android:shape="ring"
|
||||
android:thickness="1dp"
|
||||
android:useLevel="false">
|
||||
<solid android:color="#10FFFFFF" />
|
||||
</shape>
|
10
vector/src/main/res/drawable/ic_down_arrow.xml
Normal file
10
vector/src/main/res/drawable/ic_down_arrow.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="18"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:pathData="M9.8,2.7C9.8,2.2582 9.4418,1.9 9,1.9C8.5582,1.9 8.2,2.2582 8.2,2.7L8.2,13.3686L3.7157,8.8843C3.4032,8.5719 2.8967,8.5719 2.5843,8.8843C2.2719,9.1967 2.2719,9.7033 2.5843,10.0157L8.4343,15.8657C8.7467,16.1781 9.2532,16.1781 9.5657,15.8657L15.4157,10.0157C15.7281,9.7033 15.7281,9.1967 15.4157,8.8843C15.1032,8.5719 14.5967,8.5719 14.2843,8.8843L9.8,13.3686L9.8,2.7Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_media_error.xml
Normal file
10
vector/src/main/res/drawable/ic_media_error.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,24C18.6274,24 24,18.6274 24,12C24,5.3726 18.6274,0 12,0C5.3726,0 0,5.3726 0,12C0,18.6274 5.3726,24 12,24ZM10.4709,6.7651C10.3959,5.9101 11.0259,5.1601 11.8809,5.1001C12.7209,5.0401 13.4709,5.6701 13.5609,6.5251V6.7651L13.0809,12.7651C13.0359,13.3201 12.5709,13.7401 12.0159,13.7401H11.9259C11.4009,13.6951 10.9959,13.2901 10.9509,12.7651L10.4709,6.7651ZM13.3202,16.6804C13.3202,17.4094 12.7292,18.0004 12.0002,18.0004C11.2712,18.0004 10.6802,17.4094 10.6802,16.6804C10.6802,15.9513 11.2712,15.3604 12.0002,15.3604C12.7292,15.3604 13.3202,15.9513 13.3202,16.6804Z"
|
||||
android:fillColor="#8D99A5"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_media_play.xml
Normal file
12
vector/src/main/res/drawable/ic_media_play.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="34dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="34"
|
||||
android:viewportHeight="34">
|
||||
<path
|
||||
android:pathData="M17,34C7.6112,34 0,26.3888 0,17C0,7.6112 7.6112,0 17,0C26.3888,0 34,7.6112 34,17C34,26.3888 26.3888,34 17,34ZM11.6875,23.7104C11.6875,24.4928 12.5453,24.9721 13.2116,24.5621L24.1161,17.8517C24.7506,17.4612 24.7506,16.5388 24.1161,16.1483L13.2116,9.4379C12.5453,9.0279 11.6875,9.5072 11.6875,10.2896L11.6875,23.7104Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#8E99A4"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
@@ -9,28 +9,59 @@
|
||||
android:id="@+id/messageThumbnailView"
|
||||
android:layout_width="375dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintHeight_min="60dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:contentDescription="@string/a11y_image"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:scaleType="centerCrop"
|
||||
tools:layout_height="300dp"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageMediaPlayView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/play_video"
|
||||
android:src="@drawable/ic_material_play_circle"
|
||||
<!-- <ProgressBar-->
|
||||
<!-- android:id="@+id/mediaDownloadProgressBar"-->
|
||||
<!-- style="?android:attr/progressBarStyleHorizontal"-->
|
||||
<!-- android:progressTint="?riotx_background"-->
|
||||
<!-- android:progressBackgroundTint="?riotx_reaction_background_off"-->
|
||||
<!-- android:layout_width="0dp"-->
|
||||
<!-- android:layout_height="4dp"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- app:layout_constraintWidth_percent="0.2"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@id/messageThumbnailView"-->
|
||||
<!-- tools:visibility="visible"-->
|
||||
<!-- android:max="100"-->
|
||||
<!-- android:min="0"-->
|
||||
<!-- android:progress="0"-->
|
||||
<!-- tools:progress="45" />-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/messageMediaPlayView"-->
|
||||
<!-- android:layout_width="40dp"-->
|
||||
<!-- android:layout_height="40dp"-->
|
||||
<!-- android:contentDescription="@string/play_video"-->
|
||||
<!-- android:src="@drawable/ic_material_play_circle"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="@id/messageThumbnailView"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@id/messageThumbnailView"-->
|
||||
<!-- tools:visibility="visible" />-->
|
||||
|
||||
<im.vector.app.core.platform.TimelineMediaStateView
|
||||
android:id="@+id/messageMediaStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
|
||||
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
|
||||
app:layout_constraintStart_toStartOf="@id/messageThumbnailView"
|
||||
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/messageMediaUploadProgressLayout"
|
||||
|
64
vector/src/main/res/layout/media_view_state.xml
Normal file
64
vector/src/main/res/layout/media_view_state.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge 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"
|
||||
tools:parentTag="android.widget.FrameLayout">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediaStateNotDownloaded"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:contentDescription="@string/a11y_download_media"
|
||||
android:src="@drawable/ic_download"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/circularProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true">
|
||||
<include android:id="@+id/mediaProgressView" layout="@layout/view_media_progress" />
|
||||
</FrameLayout>
|
||||
<!-- <ProgressBar-->
|
||||
<!-- android:id="@+id/mediaDownloadProgressBar"-->
|
||||
<!-- style="?android:attr/progressBarStyleHorizontal"-->
|
||||
<!-- android:layout_width="40dp"-->
|
||||
<!-- android:layout_height="6dp"-->
|
||||
<!-- android:layout_centerInParent="true"-->
|
||||
<!-- android:max="100"-->
|
||||
<!-- android:min="0"-->
|
||||
<!-- android:progress="0"-->
|
||||
<!-- android:progressBackgroundTint="@color/white"-->
|
||||
<!-- android:progressTint="@color/white"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- tools:progress="45"-->
|
||||
<!-- tools:visibility="visible" />-->
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediaStateError"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:contentDescription="@string/a11y_media_permanent_error"
|
||||
android:src="@drawable/ic_media_error" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediaStatePlay"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:contentDescription="@string/play_video"
|
||||
android:src="@drawable/ic_media_play"
|
||||
app:tint="?riotx_reaction_background_off" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</merge>
|
37
vector/src/main/res/layout/view_media_progress.xml
Normal file
37
vector/src/main/res/layout/view_media_progress.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout 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:id="@+id/mediaProgressView"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="@color/black_alpha">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/circular_progress_shape"
|
||||
android:indeterminate="false"
|
||||
android:progressDrawable="@drawable/circular_progress_bar"
|
||||
android:indeterminateDrawable="@drawable/circular_progress_bar_indeterminate"
|
||||
android:textAlignment="center"
|
||||
tools:max="100"
|
||||
tools:progress="40" />
|
||||
|
||||
<!-- cancel, autodownload off not yet managed -->
|
||||
<ImageView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/progressIconImage"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/ic_cross"
|
||||
app:tint="@color/white"
|
||||
tools:contentDescription="@string/play_video"
|
||||
tools:src="@drawable/ic_down_arrow" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
@@ -16,6 +16,20 @@
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="autoplay_gif">
|
||||
<item>@string/auto_play_never</item>
|
||||
<item>@string/auto_play_direct_message</item>
|
||||
<item>@string/auto_play_direct_messages_and_private_rooms</item>
|
||||
<item>@string/auto_play_always</item>
|
||||
</string-array>
|
||||
<string-array name="autoplay_gif_values">
|
||||
<item>never</item>
|
||||
<item>dm</item>
|
||||
<item>dm_private</item>
|
||||
<item>always</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="media_sources">
|
||||
<item>@string/media_source_choose</item>
|
||||
<item>@string/option_send_files</item>
|
||||
|
@@ -692,6 +692,13 @@
|
||||
<string name="compression_opt_list_medium">Medium</string>
|
||||
<string name="compression_opt_list_small">Small</string>
|
||||
|
||||
<!-- Autoplay gif option -->
|
||||
<string name="auto_play_never">Never</string>
|
||||
<string name="auto_play_direct_message">In Direct Messages</string>
|
||||
<string name="auto_play_direct_messages_and_private_rooms">In Direct Messages and Private Rooms</string>
|
||||
<string name="auto_play_always">Always</string>
|
||||
|
||||
|
||||
<!-- media upload / download messages -->
|
||||
<string name="attachment_cancel_download">"Cancel the download?</string>
|
||||
<string name="attachment_cancel_upload">Cancel the upload?</string>
|
||||
@@ -1223,6 +1230,7 @@
|
||||
<string name="settings_keep_media">Keep media</string>
|
||||
<string name="settings_clear_cache">Clear cache</string>
|
||||
<string name="settings_clear_media_cache">Clear media cache</string>
|
||||
<string name="settings_storage_usage">Storage Usage</string>
|
||||
|
||||
<string name="settings_user_settings">User settings</string>
|
||||
<string name="settings_notifications">Notifications</string>
|
||||
@@ -1370,6 +1378,7 @@
|
||||
<!-- Media settings -->
|
||||
<string name="settings_media">Media</string>
|
||||
<string name="settings_default_compression">Default compression</string>
|
||||
<string name="settings_autoplay_gif">Autoplay Gif</string>
|
||||
<string name="compression_opt_list_choose">Choose</string>
|
||||
<string name="settings_default_media_source">Default media source</string>
|
||||
<string name="media_source_choose">Choose</string>
|
||||
@@ -3270,6 +3279,8 @@
|
||||
<string name="a11y_view_read_receipts">View read receipts</string>
|
||||
<string name="a11y_public_room">This room is public</string>
|
||||
<string name="a11y_public_space">This Space is public</string>
|
||||
<string name="a11y_download_media">Tap to download media</string>
|
||||
<string name="a11y_media_permanent_error">Failed to read media</string>
|
||||
|
||||
<string name="dev_tools_menu_name">Dev Tools</string>
|
||||
<string name="dev_tools_explore_room_state">Explore Room State</string>
|
||||
|
@@ -94,6 +94,11 @@
|
||||
android:key="SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"
|
||||
android:title="@string/settings_clear_media_cache" />
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:key="SETTINGS_STORAGE_USAGE_PREFERENCE_KEY"
|
||||
android:title="@string/settings_storage_usage"
|
||||
app:fragment="im.vector.app.features.settings.data.DataAndStorageFragment" />
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:key="SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
|
||||
android:title="@string/settings_clear_cache" />
|
||||
|
@@ -130,6 +130,14 @@
|
||||
android:title="@string/settings_vibrate_on_mention"
|
||||
app:isPreferenceVisible="@bool/false_not_implemented" />
|
||||
|
||||
<im.vector.app.core.preference.VectorListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/autoplay_gif"
|
||||
android:entryValues="@array/autoplay_gif_values"
|
||||
android:key="SETTINGS_AUTOPLAY_GIF_PREFERENCE"
|
||||
android:summary="%s"
|
||||
android:title="@string/settings_autoplay_gif" />
|
||||
|
||||
</im.vector.app.core.preference.VectorPreferenceCategory>
|
||||
|
||||
<im.vector.app.core.preference.VectorPreferenceCategory
|
||||
|
Reference in New Issue
Block a user