diff --git a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt
index 78cc0b1658e..0810621f529 100644
--- a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt
+++ b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt
@@ -29,6 +29,7 @@ import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
import com.datadog.android.rum.tracking.ComponentPredicate
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.ui.WireActivity
+import com.wire.android.util.DeviceUtil
import com.wire.android.util.getDeviceIdString
import com.wire.android.util.getGitBuildId
import com.wire.android.util.sha256
@@ -72,10 +73,15 @@ object ExternalLoggerManager {
.useSite(DatadogSite.EU1)
.build()
+ val availableMemorySize = DeviceUtil.getAvailableInternalMemorySize()
+ val totalMemorySize = DeviceUtil.getTotalInternalMemorySize()
+ val deviceParams = mapOf("available_memory_size" to availableMemorySize, "total_memory_size" to totalMemorySize)
+
val credentials = Credentials(clientToken, environmentName, appVariantName, applicationId)
val extraInfo = mapOf(
"encrypted_proteus_storage_enabled" to runBlocking { globalDataStore.isEncryptedProteusStorageEnabled().first() },
- "git_commit_hash" to context.getGitBuildId()
+ "git_commit_hash" to context.getGitBuildId(),
+ "device_params" to deviceParams
)
Datadog.initialize(context, credentials, configuration, TrackingConsent.GRANTED)
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt
index 8bdc8879867..946457b3a97 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt
@@ -49,6 +49,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import okio.Path
+import java.io.FileNotFoundException
import javax.inject.Inject
@HiltViewModel
@@ -113,24 +114,31 @@ class AvatarPickerViewModel @Inject constructor(
pictureState = PictureState.Uploading(imgUri)
val avatarPath = defaultAvatarPath
- val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong()
- when (val result = uploadUserAvatar(avatarPath, imageDataSize)) {
- is UploadAvatarResult.Success -> {
- dataStore.updateUserAvatarAssetId(result.userAssetId.toString())
- onComplete(dataStore.avatarAssetId.first())
- }
- is UploadAvatarResult.Failure -> {
- when (result.coreFailure) {
- is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError)
- else -> showInfoMessage(InfoMessageType.UploadAvatarError)
+ try {
+ val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong()
+
+ when (val result = uploadUserAvatar(avatarPath, imageDataSize)) {
+ is UploadAvatarResult.Success -> {
+ dataStore.updateUserAvatarAssetId(result.userAssetId.toString())
+ onComplete(dataStore.avatarAssetId.first())
}
- with(initialPictureLoadingState) {
- pictureState = when (this) {
- is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri)
- else -> PictureState.Empty
+
+ is UploadAvatarResult.Failure -> {
+ when (result.coreFailure) {
+ is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError)
+ else -> showInfoMessage(InfoMessageType.UploadAvatarError)
+ }
+ with(initialPictureLoadingState) {
+ pictureState = when (this) {
+ is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri)
+ else -> PictureState.Empty
+ }
}
}
}
+ } catch (e: FileNotFoundException) {
+ appLogger.e("[AvatarPickerViewModel] Could not find a file", e)
+ showInfoMessage(InfoMessageType.ImageProcessError)
}
}
}
@@ -157,5 +165,6 @@ class AvatarPickerViewModel @Inject constructor(
sealed class InfoMessageType(override val uiText: UIText) : SnackBarMessage {
data object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar))
data object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message))
+ data object ImageProcessError : InfoMessageType(UIText.StringResource(R.string.error_process_user_avatar))
}
}
diff --git a/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt
new file mode 100644
index 00000000000..74ad6cbbffb
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt
@@ -0,0 +1,77 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.util
+
+import android.os.Environment
+import android.os.StatFs
+
+object DeviceUtil {
+ private const val BYTES_IN_KILOBYTE = 1024
+ private const val BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024
+ private const val BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024
+ private const val DIGITS_GROUP_SIZE = 3 // Number of digits between commas in formatted size.
+
+ fun getAvailableInternalMemorySize(): String = try {
+ val path = Environment.getDataDirectory()
+ val stat = StatFs(path.path)
+ val blockSize = stat.blockSizeLong
+ val availableBlocks = stat.availableBlocksLong
+ formatSize(availableBlocks * blockSize)
+ } catch (e: IllegalArgumentException) {
+ ""
+ }
+
+ fun getTotalInternalMemorySize(): String = try {
+ val path = Environment.getDataDirectory()
+ val stat = StatFs(path.path)
+ val blockSize = stat.blockSizeLong
+ val totalBlocks = stat.blockCountLong
+ formatSize(totalBlocks * blockSize)
+ } catch (e: IllegalArgumentException) {
+ ""
+ }
+
+ private fun formatSize(sizeInBytes: Long): String {
+ var size = sizeInBytes
+ var suffix: String? = null
+ when {
+ size >= BYTES_IN_GIGABYTE -> {
+ suffix = "GB"
+ size /= BYTES_IN_GIGABYTE
+ }
+
+ size >= BYTES_IN_MEGABYTE -> {
+ suffix = "MB"
+ size /= BYTES_IN_MEGABYTE
+ }
+
+ size >= BYTES_IN_KILOBYTE -> {
+ suffix = "KB"
+ size /= BYTES_IN_KILOBYTE
+ }
+ }
+ val resultBuffer = StringBuilder(size.toString())
+ var commaOffset = resultBuffer.length - DIGITS_GROUP_SIZE
+ while (commaOffset > 0) {
+ resultBuffer.insert(commaOffset, ',')
+ commaOffset -= DIGITS_GROUP_SIZE
+ }
+ suffix?.let { resultBuffer.append(it) }
+ return resultBuffer.toString()
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
index 4a66f7c3b2e..667b5765e5e 100644
--- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
+++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
@@ -27,7 +27,6 @@ import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.database.Cursor
-import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
@@ -42,10 +41,7 @@ import android.provider.MediaStore.MediaColumns.SIZE
import android.provider.OpenableColumns
import android.provider.Settings
import android.webkit.MimeTypeMap
-import androidx.annotation.AnyRes
-import androidx.annotation.NonNull
import androidx.annotation.VisibleForTesting
-import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.wire.android.R
import com.wire.android.appLogger
@@ -62,28 +58,11 @@ import kotlinx.serialization.json.Json
import okio.Path
import java.io.File
import java.io.FileNotFoundException
+import java.io.IOException
import java.io.InputStream
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
-/**
- * Gets the uri of any drawable or given resource
- * @param context - context
- * @param drawableId - drawable res id
- * @return - uri
- */
-fun getUriFromDrawable(
- @NonNull context: Context,
- @AnyRes drawableId: Int
-): Uri {
- return Uri.parse(
- ContentResolver.SCHEME_ANDROID_RESOURCE +
- "://" + context.resources.getResourcePackageName(drawableId) +
- '/' + context.resources.getResourceTypeName(drawableId) +
- '/' + context.resources.getResourceEntryName(drawableId)
- )
-}
-
@Suppress("MagicNumber")
suspend fun Uri.toByteArray(context: Context, dispatcher: DispatcherProvider = DefaultDispatcherProvider()): ByteArray {
return withContext(dispatcher.io()) {
@@ -91,21 +70,6 @@ suspend fun Uri.toByteArray(context: Context, dispatcher: DispatcherProvider = D
}
}
-suspend fun Uri.toDrawable(context: Context, dispatcher: DispatcherProvider = DefaultDispatcherProvider()): Drawable? {
- val dataUri = this
- return withContext(dispatcher.io()) {
- try {
- context.contentResolver.openInputStream(dataUri).use { inputStream ->
- Drawable.createFromStream(inputStream, dataUri.toString())
- }
- } catch (e: FileNotFoundException) {
- defaultGalleryIcon(context)
- }
- }
-}
-
-private fun defaultGalleryIcon(context: Context) = ContextCompat.getDrawable(context, R.drawable.ic_gallery)
-
fun getTempWritableAttachmentUri(context: Context, attachmentPath: Path): Uri {
val file = attachmentPath.toFile()
file.setWritable(true)
@@ -243,9 +207,17 @@ suspend fun Uri.resampleImageAndCopyToTempPath(
ImageUtil.resample(originalImage, sizeClass).let { processedImage ->
val file = tempCachePath.toFile()
- size = processedImage.size.toLong()
- file.setWritable(true)
- file.outputStream().use { it.write(processedImage) }
+ try {
+ size = processedImage.size.toLong()
+ file.setWritable(true)
+ file.outputStream().use { it.write(processedImage) }
+ } catch (e: FileNotFoundException) {
+ appLogger.e("[ResampleImage] Cannot find file ${file.path}", e)
+ throw e
+ } catch (e: IOException) {
+ appLogger.e("[ResampleImage] I/O error while writing the image", e)
+ throw e
+ }
}
size
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ac1b79197a6..4b66ac39640 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -774,6 +774,7 @@
Please check your Internet connection and try again
There was an error downloading your profile picture. Please check your Internet connection
Picture could not be uploaded
+ Picture could not be processed
Image upload failed
Image download failed
Notifications could not be updated