Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ dependencies {

implementation("androidx.media3:media3-exoplayer:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
implementation("androidx.media3:media3-transformer:$media3Version")
implementation("androidx.media3:media3-effect:$media3Version")

implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("pl.droidsonroids.gif:android-gif-drawable:1.2.31")
Expand Down
63 changes: 20 additions & 43 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2330,19 +2330,11 @@ class ChatActivity :
try {
require(filesToUpload.isNotEmpty())

val filenamesWithLineBreaks = StringBuilder("\n")

for (file in filesToUpload) {
val filename = FileUtils.getFileName(file, context)
filenamesWithLineBreaks.append(filename).append("\n")
}

val newFragment = FileAttachmentPreviewFragment.newInstance(
filenamesWithLineBreaks.toString(),
filesToUpload.map { it.toString() }.toMutableList()
)
newFragment.setListener { files, caption ->
uploadFiles(files, caption)
newFragment.setListener { files, caption, compressImages ->
uploadFiles(files, caption, compressImages)
}
newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
} catch (e: IllegalStateException) {
Expand Down Expand Up @@ -2426,18 +2418,10 @@ class ChatActivity :
}

if (permissionUtil.isFilesPermissionGranted()) {
val filenamesWithLineBreaks = StringBuilder("\n")

for (file in filesToUpload) {
val filename = FileUtils.getFileName(file.toUri(), context)
filenamesWithLineBreaks.append(filename).append("\n")
val newFragment = FileAttachmentPreviewFragment.newInstance(filesToUpload)
newFragment.setListener { files, caption, compressImages ->
uploadFiles(files, caption, compressImages)
}

val newFragment = FileAttachmentPreviewFragment.newInstance(
filenamesWithLineBreaks.toString(),
filesToUpload
)
newFragment.setListener { files, caption -> uploadFiles(files, caption) }
newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
} else {
UploadAndShareFilesWorker.requestStoragePermission(this)
Expand Down Expand Up @@ -2545,27 +2529,17 @@ class ChatActivity :
}
}

private fun uploadFiles(files: MutableList<String>, caption: String = "") {
private fun uploadFiles(files: MutableList<String>, caption: String = "", compressImages: Boolean = false) {
for (i in 0 until files.size) {
if (i == files.size - 1) {
uploadFile(
fileUri = files[i],
isVoiceMessage = false,
caption = caption,
roomToken = roomToken,
replyToMessageId = getReplyToMessageId(),
displayName = currentConversation?.displayName!!
)
} else {
uploadFile(
fileUri = files[i],
isVoiceMessage = false,
caption = "",
roomToken = roomToken,
replyToMessageId = getReplyToMessageId(),
displayName = currentConversation?.displayName!!
)
}
uploadFile(
fileUri = files[i],
isVoiceMessage = false,
caption = if (i == files.size - 1) caption else "",
roomToken = roomToken,
replyToMessageId = getReplyToMessageId(),
displayName = currentConversation?.displayName!!,
compressImages = compressImages
)
}
}

Expand Down Expand Up @@ -3945,21 +3919,24 @@ class ChatActivity :
)
}

@Suppress("LongParameterList")
fun uploadFile(
fileUri: String,
isVoiceMessage: Boolean,
caption: String = "",
roomToken: String = "",
replyToMessageId: Int? = null,
displayName: String
displayName: String,
compressImages: Boolean = false
) {
chatViewModel.uploadFile(
fileUri,
isVoiceMessage,
caption,
roomToken,
replyToMessageId,
displayName
displayName,
compressImages
)
cancelReply()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,6 @@ class MessageInputFragment : Fragment() {

override fun onSuccess(result: Drawable) {
binding.fragmentCallStarted.callAuthorChip.chipIcon = result

}
})
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1972,7 +1972,8 @@ class ChatViewModel @AssistedInject constructor(
caption: String = "",
roomToken: String = "",
replyToMessageId: Int? = null,
displayName: String
displayName: String,
compressImages: Boolean = false
) {
val metaDataMap = mutableMapOf<String, Any>()
var room = ""
Expand Down Expand Up @@ -2004,7 +2005,8 @@ class ChatViewModel @AssistedInject constructor(
fileUri,
room,
displayName,
metaData
metaData,
compressImages
)
} catch (e: IllegalArgumentException) {
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.ImageCompressor
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.RemoteFileUtils
import com.nextcloud.talk.utils.VideoCompressor
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
Expand Down Expand Up @@ -115,15 +117,21 @@
require(sourceFile.isNotEmpty())
checkNotNull(roomToken)

val sourceFileUri = sourceFile.toUri()
var sourceFileUri = sourceFile.toUri()
fileName = FileUtils.getFileName(sourceFileUri, context)
file = FileUtils.getFileFromUri(context, sourceFileUri)

initNotificationSetup()

if (inputData.getBoolean(COMPRESS_IMAGES, false)) {
sourceFileUri = compressMediaIfPossible(sourceFileUri)
}

val remotePath = getRemotePath(currentUser)

val useConversationSubfolders = CapabilitiesUtil.hasConversationSubfoldersForAttachments(
currentUser.capabilities!!.spreedCapability!!
)
initNotificationSetup()
file?.let { isChunkedUploading = it.length() > CHUNK_UPLOAD_THRESHOLD_SIZE }
val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath, useConversationSubfolders)

Expand All @@ -146,6 +154,35 @@
}
}

/**
* Replaces [file] and [fileName] with a compressed copy if [sourceFileUri] points to a
* compressible image or video, returning the [Uri] that should be uploaded.
*/
@Suppress("ReturnCount")
private fun compressMediaIfPossible(sourceFileUri: Uri): Uri {
val originalFile = file ?: return sourceFileUri
val mimeType = FileUtils.resolveMimeType(context, sourceFileUri)

val compressedFile = when {
ImageCompressor.isCompressible(mimeType) -> ImageCompressor.compress(context, originalFile)
VideoCompressor.isCompressible(mimeType) -> compressVideoWithProgress(originalFile)
else -> null
} ?: return sourceFileUri

file = compressedFile
fileName = compressedFile.name
return Uri.fromFile(compressedFile)
}

/**
* Video compression can take a while, so the upload notification is repurposed to show its
* progress before it transitions into the actual upload progress.
*/
private fun compressVideoWithProgress(originalFile: File): File? {
showCompressionStartedNotification()
return VideoCompressor.compress(context, originalFile, onProgress = ::onCompressionProgress)
}

private fun uploadFile(
sourceFileUri: Uri,
metaData: String?,
Expand Down Expand Up @@ -285,6 +322,7 @@
NotificationUtils.NotificationChannels
.NOTIFICATION_CHANNEL_UPLOADS.name
)
notificationId = SystemClock.uptimeMillis().toInt()
}

private fun initNotificationWithPercentage() {
Expand All @@ -304,13 +342,54 @@
)
.build()

notificationId = SystemClock.uptimeMillis().toInt()
mNotifyManager!!.notify(notificationId, initNotification)
// only need one summary notification but multiple upload worker can call it more than once but it is safe
// because of the same notification object config and id.
makeSummaryNotification()
}

/**
* Shows the same upload notification, but reflecting the compression phase that precedes the
* actual upload. Reuses [notificationId] so it later morphs into the upload progress notification
* instead of appearing as a separate entry.
*/
private fun showCompressionStartedNotification() {
val compressionNotification = mBuilder!!
.setContentTitle(context.resources.getString(R.string.nc_compress_in_progress))
.setContentText(getCompressionNotificationContentText(ZERO_PERCENT))
.setSmallIcon(R.drawable.upload_white)
.setOngoing(true)
.setProgress(HUNDRED_PERCENT, ZERO_PERCENT, false)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setGroup(NotificationUtils.KEY_UPLOAD_GROUP)
.setContentIntent(getIntentToOpenConversation())
.addAction(
R.drawable.ic_cancel_white_24dp,
getResourceString(context, R.string.nc_cancel),
getCancelUploadIntent()
)
.build()

mNotifyManager!!.notify(notificationId, compressionNotification)

Check failure

Code scanning / CodeQL

Use of implicit PendingIntents High

An implicit Intent is created
and sent to an unspecified third party through a PendingIntent.
makeSummaryNotification()
}

private fun onCompressionProgress(percentage: Int) {
val progressUpdateNotification = mBuilder!!
.setProgress(HUNDRED_PERCENT, percentage, false)
.setContentText(getCompressionNotificationContentText(percentage))
.build()

mNotifyManager!!.notify(notificationId, progressUpdateNotification)
}

private fun getCompressionNotificationContentText(percentage: Int): String =
String.format(
getResourceString(context, R.string.nc_compress_notification_text),
getShortenedFileName(),
percentage
)

private fun makeSummaryNotification() {
// summary notification encapsulating the group of notifications
val summaryNotification = NotificationCompat.Builder(
Expand Down Expand Up @@ -413,6 +492,7 @@
private const val ROOM_TOKEN = "ROOM_TOKEN"
private const val CONVERSATION_NAME = "CONVERSATION_NAME"
private const val META_DATA = "META_DATA"
private const val COMPRESS_IMAGES = "COMPRESS_IMAGES"
private const val CHUNK_UPLOAD_THRESHOLD_SIZE: Long = 1024 * 1024
private const val NOTIFICATION_FILE_NAME_MAX_LENGTH = 20
private const val THREE_DOTS = "…"
Expand Down Expand Up @@ -465,12 +545,19 @@
}
}

fun upload(fileUri: String, roomToken: String, conversationName: String, metaData: String?) {
fun upload(
fileUri: String,
roomToken: String,
conversationName: String,
metaData: String?,
compressImages: Boolean = false
) {
val data: Data = Data.Builder()
.putString(DEVICE_SOURCE_FILE, fileUri)
.putString(ROOM_TOKEN, roomToken)
.putString(CONVERSATION_NAME, conversationName)
.putString(META_DATA, metaData)
.putBoolean(COMPRESS_IMAGES, compressImages)
.build()
val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
.setInputData(data)
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ class SettingsActivity :
settingsScreenLockSwitch,
settingsScreenSecuritySwitch,
settingsIncognitoKeyboardSwitch,
settingsCompressUploadImagesSwitch,
settingsPhoneBookIntegrationSwitch,
settingsReadPrivacySwitch,
settingsTypingStatusSwitch,
Expand Down Expand Up @@ -1293,6 +1294,7 @@ class SettingsActivity :
appPreferences.setIncognitoKeyboard(!isChecked)
}

setupCompressUploadImagesSetting()
setupPhoneBookIntegrationSetting()

binding.settingsScreenSecuritySwitch.isChecked = appPreferences.isScreenSecured
Expand All @@ -1318,6 +1320,15 @@ class SettingsActivity :
}
}

private fun setupCompressUploadImagesSetting() {
binding.settingsCompressUploadImagesSwitch.isChecked = appPreferences.compressUploadImages
binding.settingsCompressUploadImages.setOnClickListener {
val isChecked = binding.settingsCompressUploadImagesSwitch.isChecked
binding.settingsCompressUploadImagesSwitch.isChecked = !isChecked
appPreferences.setCompressUploadImages(!isChecked)
}
}

private fun setupPhoneBookIntegrationSetting() {
binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled
binding.settingsPhoneBookIntegration.setOnClickListener {
Expand Down
Loading
Loading