Skip to content

Commit

Permalink
chore: file access crashes [WPB-7368] (#2994)
Browse files Browse the repository at this point in the history
  • Loading branch information
Garzas authored May 10, 2024
1 parent faad623 commit ce608e9
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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))
}
}
77 changes: 77 additions & 0 deletions app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
52 changes: 12 additions & 40 deletions app/src/main/kotlin/com/wire/android/util/FileUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -62,50 +58,18 @@ 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()) {
context.contentResolver.openInputStream(this@toByteArray)?.use { it.readBytes() } ?: ByteArray(16)
}
}

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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@
<string name="error_no_network_message">Please check your Internet connection and try again</string>
<string name="error_downloading_self_user_profile_picture">There was an error downloading your profile picture. Please check your Internet connection</string>
<string name="error_uploading_user_avatar">Picture could not be uploaded</string>
<string name="error_process_user_avatar">Picture could not be processed</string>
<string name="error_uploading_image_message">Image upload failed</string>
<string name="error_downloading_image_message">Image download failed</string>
<string name="error_updating_muting_setting">Notifications could not be updated</string>
Expand Down

0 comments on commit ce608e9

Please sign in to comment.