diff --git a/CHANGES.md b/CHANGES.md index 889e9b4fca..0fcfabf2ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ SDK API changes ⚠️: - Build 🧱: + - Compile with Kotlin 1.5. + - Upgrade some dependencies: gradle wrapper, third party lib, etc. - Sign APK with build tools 30.0.3 Test: diff --git a/build.gradle b/build.gradle index 78d9c39036..e7fab91e74 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.32' - ext.kotlin_coroutines_version = "1.4.2" + ext.kotlin_version = '1.5.0' + ext.kotlin_coroutines_version = "1.5.0-RC" repositories { google() jcenter() @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.0' classpath 'com.google.gms:google-services:4.3.5' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 099f10061c..e1e2fd2c75 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=ca42877db3519b667cd531c414be517b294b0467059d401e7133f0e55b9bf265 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-all.zip +distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt index ee604fc9ab..76bf6dc040 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -226,12 +226,12 @@ class QrCodeTest : InstrumentedTest { private fun checkHeader(byteArray: ByteArray) { // MATRIX - byteArray[0] shouldBeEqualTo 'M'.toByte() - byteArray[1] shouldBeEqualTo 'A'.toByte() - byteArray[2] shouldBeEqualTo 'T'.toByte() - byteArray[3] shouldBeEqualTo 'R'.toByte() - byteArray[4] shouldBeEqualTo 'I'.toByte() - byteArray[5] shouldBeEqualTo 'X'.toByte() + byteArray[0] shouldBeEqualTo 'M'.code.toByte() + byteArray[1] shouldBeEqualTo 'A'.code.toByte() + byteArray[2] shouldBeEqualTo 'T'.code.toByte() + byteArray[3] shouldBeEqualTo 'R'.code.toByte() + byteArray[4] shouldBeEqualTo 'I'.code.toByte() + byteArray[5] shouldBeEqualTo 'X'.code.toByte() // Version byteArray[6] shouldBeEqualTo 2 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt index 2c4c03b7d4..7a4231c277 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.safeCapitalize /** * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 @@ -39,6 +40,6 @@ data class WidgetContent( @SuppressLint("DefaultLocale") fun getHumanName(): String { - return (name ?: type ?: "").capitalize() + return (name ?: type ?: "").safeCapitalize() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index 7b2fae86ef..3bf3f66e40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -117,22 +117,22 @@ sealed class MatrixItem( var first = dn[startIndex] // LEFT-TO-RIGHT MARK - if (dn.length >= 2 && 0x200e == first.toInt()) { + if (dn.length >= 2 && 0x200e == first.code) { startIndex++ first = dn[startIndex] } // check if it’s the start of a surrogate pair - if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) { + if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { val second = dn[startIndex + 1] - if (second.toInt() in 0xDC00..0xDFFF) { + if (second.code in 0xDC00..0xDFFF) { length++ } } dn.substring(startIndex, startIndex + length) } - .toUpperCase(Locale.ROOT) + .uppercase(Locale.ROOT) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index c7885ce449..4bf01a2809 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -345,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { + if ("sha256" == accepted?.hash?.lowercase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -355,7 +355,7 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) { SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) else -> null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt index 6bc3483e65..76e88442b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt @@ -48,7 +48,7 @@ fun QrCodeData.toEncodedString(): String { // TransactionId transactionId.forEach { - result += it.toByte() + result += it.code.toByte() } // Keys diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 891858d857..a284d976d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -291,7 +291,7 @@ internal class DefaultFileService @Inject constructor( Timber.v("Get size of ${it.absolutePath}") true } - .sumBy { it.length().toInt() } + .sumOf { it.length().toInt() } } override fun clearCache() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 4f6e906766..114695062c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -117,7 +117,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return withOlmUtility { olmUtility -> threePids.map { threePid -> base64ToBase64Url( - olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) + " " + threePid.toMedium() + " " + pepper) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index e19b1bcca7..47f20913ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -27,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase(Locale.ROOT) + .lowercase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index 2fabca4be8..aa0b92aa45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import timber.log.Timber +import java.util.Locale /** * Convert a string to an UTF8 String @@ -24,7 +25,7 @@ import timber.log.Timber * @param s the string to convert * @return the utf-8 string */ -fun convertToUTF8(s: String): String { +internal fun convertToUTF8(s: String): String { return try { val bytes = s.toByteArray(Charsets.UTF_8) String(bytes) @@ -40,7 +41,7 @@ fun convertToUTF8(s: String): String { * @param s the string to convert * @return the utf-16 string */ -fun convertFromUTF8(s: String): String { +internal fun convertFromUTF8(s: String): String { return try { val bytes = s.toByteArray() String(bytes, Charsets.UTF_8) @@ -56,7 +57,7 @@ fun convertFromUTF8(s: String): String { * @param subString the string to search for * @return whether a match was found */ -fun String.caseInsensitiveFind(subString: String): Boolean { +internal fun String.caseInsensitiveFind(subString: String): Boolean { // add sanity checks if (subString.isEmpty() || isEmpty()) { return false @@ -78,3 +79,14 @@ internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex() * Strip all the UTF-8 chars which are actually spaces */ internal fun String.replaceSpaceChars() = replace(spaceChars, "") + +// String.capitalize is now deprecated +internal fun String.safeCapitalize(): String { + return replaceFirstChar { char -> + if (char.isLowerCase()) { + char.titlecase(Locale.getDefault()) + } else { + char.toString() + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index 15c7e88bac..d429b293b2 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -19,13 +19,14 @@ import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -38,7 +39,8 @@ import javax.inject.Inject class TestPushFromPushGateway @Inject constructor(private val context: AppCompatActivity, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, - private val pushersManager: PushersManager) + private val pushersManager: PushersManager, + private val activeSessionHolder: ActiveSessionHolder) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null @@ -50,7 +52,7 @@ class TestPushFromPushGateway @Inject constructor(private val context: AppCompat status = TestStatus.FAILED return } - action = GlobalScope.launch { + action = activeSessionHolder.getActiveSession().coroutineScope.launch { val result = runCatching { pushersManager.testPush(fcmToken) } withContext(Dispatchers.Main) { diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index a2a242a3d9..c5eac7e3e0 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -22,10 +22,10 @@ import androidx.lifecycle.OnLifecycleEvent import arrow.core.Option import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.BehaviorDataSource +import im.vector.app.features.session.coroutineScope import im.vector.app.features.ui.UiStateRepository import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -63,30 +63,30 @@ class AppStateHandler @Inject constructor( fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? = selectedSpaceDataSource.currentValue?.orNull() fun setCurrentSpace(spaceId: String?, session: Session? = null) { - val uSession = session ?: activeSessionHolder.getSafeActiveSession() + val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return - val spaceSum = spaceId?.let { uSession?.getRoomSummary(spaceId) } + val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { - GlobalScope.launch(Dispatchers.IO) { + uSession.coroutineScope.launch(Dispatchers.IO) { tryOrNull { - uSession?.getRoom(spaceId)?.loadRoomMembersIfNeeded() + uSession.getRoom(spaceId)?.loadRoomMembersIfNeeded() } } } } fun setCurrentGroup(groupId: String?, session: Session? = null) { - val uSession = session ?: activeSessionHolder.getSafeActiveSession() + val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.ByLegacyGroup && groupId == selectedSpaceDataSource.currentValue?.orNull()?.group()?.groupId) return - val activeGroup = groupId?.let { uSession?.getGroupSummary(groupId) } + val activeGroup = groupId?.let { uSession.getGroupSummary(groupId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.ByLegacyGroup(activeGroup))) if (groupId != null) { - GlobalScope.launch { + uSession.coroutineScope.launch { tryOrNull { - uSession?.getGroup(groupId)?.fetchGroupData() + uSession.getGroup(groupId)?.fetchGroupData() } } } diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index 1299f4086b..e68b5e1b07 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -45,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { if (null != mimeType) { // the mimetype is sometimes in uppercase. - mimeType = mimeType.toLowerCase(Locale.ROOT) + mimeType = mimeType.lowercase(Locale.ROOT) } } catch (e: Exception) { Timber.e(e, "Failed to open resource input stream") diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt index 66907ded10..d73af1e917 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt @@ -42,13 +42,13 @@ fun CharSequence.splitEmoji(): List { while (index < length) { val firstChar = get(index) - if (firstChar.toInt() == 0x200e) { + if (firstChar.code == 0x200e) { // Left to right mark. What should I do with it? - } else if (firstChar.toInt() in 0xD800..0xDBFF && index + 1 < length) { + } else if (firstChar.code in 0xD800..0xDBFF && index + 1 < length) { // We have the start of a surrogate pair val secondChar = get(index + 1) - if (secondChar.toInt() in 0xDC00..0xDFFF) { + if (secondChar.code in 0xDC00..0xDFFF) { // We have an emoji result.add("$firstChar$secondChar") index++ diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 90cbb3a7a5..d82de1b4ed 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -43,8 +43,7 @@ import im.vector.app.R import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.themes.ThemeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okio.buffer import okio.sink import okio.source @@ -57,6 +56,7 @@ import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.lang.IllegalStateException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -344,90 +344,93 @@ private fun appendTimeToFilename(name: String): String { return """${filename}_$dateExtension.$fileExtension""" } -fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val filename = appendTimeToFilename(title) +suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { + withContext(Dispatchers.IO) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val filename = appendTimeToFilename(title) - val values = ContentValues().apply { - put(MediaStore.Images.Media.TITLE, filename) - put(MediaStore.Images.Media.DISPLAY_NAME, filename) - put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - } - val externalContentUri = when { - mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI - } + val values = ContentValues().apply { + put(MediaStore.Images.Media.TITLE, filename) + put(MediaStore.Images.Media.DISPLAY_NAME, filename) + put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) + put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + } + val externalContentUri = when { + mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI + } - val uri = context.contentResolver.insert(externalContentUri, values) - if (uri == null) { - Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() - } else { - val source = file.inputStream().source().buffer() - context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> - source.use { input -> - sink.use { output -> - output.writeAll(input) + val uri = context.contentResolver.insert(externalContentUri, values) + if (uri == null) { + Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) + } else { + val source = file.inputStream().source().buffer() + context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } } } + notificationUtils.buildDownloadFileNotification( + uri, + filename, + mediaMimeType ?: MimeTypes.OctetStream + ).let { notification -> + notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) + } } - notificationUtils.buildDownloadFileNotification( - uri, - filename, - mediaMimeType ?: MimeTypes.OctetStream - ).let { notification -> - notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) - } + } else { + saveMediaLegacy(context, mediaMimeType, title, file) } - } else { - saveMediaLegacy(context, mediaMimeType, title, file) } } @Suppress("DEPRECATION") -private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, file: File) { +private fun saveMediaLegacy(context: Context, + mediaMimeType: String?, + title: String, + file: File) { val state = Environment.getExternalStorageState() if (Environment.MEDIA_MOUNTED != state) { context.toast(context.getString(R.string.error_saving_media_file)) - return + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) } - GlobalScope.launch(Dispatchers.IO) { - val dest = when { - mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES - mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES - mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS + val dest = when { + mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES + mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES + mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC + else -> Environment.DIRECTORY_DOWNLOADS + } + val downloadDir = Environment.getExternalStoragePublicDirectory(dest) + try { + val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { + val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + "$title.$extension" + } else { + title } - val downloadDir = Environment.getExternalStoragePublicDirectory(dest) - try { - val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { - val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } - "$title.$extension" - } else { - title - } - val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) - if (savedFile != null) { - val downloadManager = context.getSystemService() - downloadManager?.addCompletedDownload( - savedFile.name, - title, - true, - mediaMimeType ?: MimeTypes.OctetStream, - savedFile.absolutePath, - savedFile.length(), - true) - addToGallery(savedFile, mediaMimeType, context) - } - } catch (error: Throwable) { - GlobalScope.launch(Dispatchers.Main) { - context.toast(context.getString(R.string.error_saving_media_file)) - } + val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) + if (savedFile != null) { + val downloadManager = context.getSystemService() + downloadManager?.addCompletedDownload( + savedFile.name, + title, + true, + mediaMimeType ?: MimeTypes.OctetStream, + savedFile.absolutePath, + savedFile.length(), + true) + addToGallery(savedFile, mediaMimeType, context) } + } catch (error: Throwable) { + context.toast(context.getString(R.string.error_saving_media_file)) + throw error } } diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index b5ce922487..7ce6dd1c67 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -112,7 +112,7 @@ fun getFileExtension(fileUri: String): String? { val ext = filename.substring(dotPos + 1) if (ext.isNotBlank()) { - return ext.toLowerCase(Locale.ROOT) + return ext.lowercase(Locale.ROOT) } } } @@ -131,5 +131,5 @@ fun getSizeOfFiles(root: File): Int { Timber.v("Get size of ${it.absolutePath}") true } - .sumBy { it.length().toInt() } + .sumOf { it.length().toInt() } } diff --git a/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt b/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt new file mode 100644 index 0000000000..6d681007ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/StringUtils.kt @@ -0,0 +1,30 @@ +/* + * 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 java.util.Locale + +// String.capitalize is now deprecated +fun String.safeCapitalize(locale: Locale): String { + return replaceFirstChar { char -> + if (char.isLowerCase()) { + char.titlecase(locale) + } else { + char.toString() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index fcd6bf0a77..a3a1a29c4b 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -33,11 +33,12 @@ import im.vector.app.features.call.utils.awaitCreateOffer import im.vector.app.features.call.utils.awaitSetLocalDescription import im.vector.app.features.call.utils.awaitSetRemoteDescription import im.vector.app.features.call.utils.mapToCallCandidate +import im.vector.app.features.session.coroutineScope import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -103,6 +104,9 @@ class WebRtcCall(val mxCall: MxCall, private val listeners = CopyOnWriteArrayList() + private val sessionScope: CoroutineScope? + get() = sessionProvider.get()?.coroutineScope + fun addListener(listener: Listener) { listeners.add(listener) } @@ -191,7 +195,7 @@ class WebRtcCall(val mxCall: MxCall, fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate) fun onRenegotiationNeeded(restartIce: Boolean) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) { Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event") return@launch @@ -262,7 +266,7 @@ class WebRtcCall(val mxCall: MxCall, localSurfaceRenderers.addIfNeeded(localViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { when (mode) { VectorCallActivity.INCOMING_ACCEPT -> { internalAcceptIncomingCall() @@ -283,7 +287,7 @@ class WebRtcCall(val mxCall: MxCall, } fun acceptIncomingCall() { - GlobalScope.launch { + sessionScope?.launch { Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") if (mxCall.state == CallState.LocalRinging) { internalAcceptIncomingCall() @@ -564,7 +568,7 @@ class WebRtcCall(val mxCall: MxCall, } fun updateRemoteOnHold(onHold: Boolean) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { if (remoteOnHold == onHold) return@launch val direction: RtpTransceiver.RtpTransceiverDirection if (onHold) { @@ -688,7 +692,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onAddStream(stream: MediaStream) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { // reportError("Weird-looking stream: " + stream); if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { Timber.e("## VOIP StreamObserver weird looking stream: $stream") @@ -712,7 +716,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onRemoveStream() { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { remoteSurfaceRenderers .mapNotNull { it.get() } .forEach { remoteVideoTrack?.removeSink(it) } @@ -734,7 +738,7 @@ class WebRtcCall(val mxCall: MxCall, } val wasRinging = mxCall.state is CallState.LocalRinging mxCall.state = CallState.Terminated - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { release() } onCallEnded(callId) @@ -750,7 +754,7 @@ class WebRtcCall(val mxCall: MxCall, // Call listener fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { iceCandidatesContent.candidates.forEach { if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) { return@forEach @@ -763,7 +767,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}") val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) try { @@ -779,7 +783,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { - GlobalScope.launch(dispatcher) { + sessionScope?.launch(dispatcher) { val description = callNegotiateContent.description val type = description?.type val sdpText = description?.sdp diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt index 282f7b1a71..c7e4c26385 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt @@ -18,8 +18,8 @@ package im.vector.app.features.crypto.keys import android.content.Context import android.net.Uri +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback @@ -33,7 +33,7 @@ class KeysExporter(private val session: Session) { * Export keys and return the file path with the callback */ fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) { - GlobalScope.launch(Dispatchers.Main) { + session.coroutineScope.launch(Dispatchers.Main) { runCatching { withContext(Dispatchers.IO) { val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt index 8932bb9489..3d93b26edd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysImporter.kt @@ -20,8 +20,8 @@ import android.content.Context import android.net.Uri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.resources.openResource +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback @@ -41,7 +41,7 @@ class KeysImporter(private val session: Session) { mimetype: String?, password: String, callback: MatrixCallback) { - GlobalScope.launch(Dispatchers.Main) { + session.coroutineScope.launch(Dispatchers.Main) { runCatching { withContext(Dispatchers.IO) { val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri)) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 9a3b5fa874..8833702e35 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -25,6 +25,7 @@ import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import arrow.core.Try import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.app.R @@ -37,7 +38,6 @@ import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentKeysBackupSetupStep3Binding import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.IOException @@ -163,7 +163,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment if (activityResult.resultCode == Activity.RESULT_OK) { val uri = activityResult.data?.data ?: return@registerStartForActivityResult - GlobalScope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { try { sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri)!!)) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index a67cb96d37..8e21412715 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session @@ -427,7 +426,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun tentativeRestoreBackup(res: Map?) { - GlobalScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { Timber.v("## Keybackup secret not restored from SSSS") diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 447a567cf4..bfedbd6f52 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -27,9 +27,9 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.ReAuthHelper +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth @@ -184,7 +184,7 @@ class HomeActivityViewModel @AssistedInject constructor( private fun maybeBootstrapCrossSigningAfterInitialSync() { // We do not use the viewModel context because we do not want to tie this action to activity view model - GlobalScope.launch(Dispatchers.IO) { + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) { val session = activeSessionHolder.getSafeActiveSession() ?: return@launch tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index cabd69ecf9..534bf55b33 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1745,20 +1745,19 @@ class RoomDetailFragment @Inject constructor( session.coroutineScope.launch { val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } if (!isAdded) return@launch - result.fold( - { - saveMedia( - context = requireContext(), - file = it, - title = action.messageContent.body, - mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), - notificationUtils = notificationUtils - ) - }, - { + result.mapCatching { + saveMedia( + context = requireContext(), + file = it, + title = action.messageContent.body, + mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), + notificationUtils = notificationUtils + ) + } + .onFailure { + if (!isAdded) return@onFailure showErrorInSnackbar(it) } - ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index cd7f6a5730..3a9969b43c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -449,7 +449,7 @@ class RoomDetailViewModel @AssistedInject constructor( widgetSessionId = widgetSessionId.substring(0, 7) } val roomId: String = room.roomId - val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale) + val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) val preferredJitsiDomain = tryOrNull { rawService.getElementWellknown(session.myUserId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt index 6a590206cb..ed343d52ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt @@ -50,7 +50,7 @@ class MatrixItemColorProvider @Inject constructor( fun getColorFromUserId(userId: String?): Int { var hash = 0 - userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() } + userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.code } return when (abs(hash) % 8) { 1 -> R.color.riotx_username_2 @@ -66,7 +66,7 @@ class MatrixItemColorProvider @Inject constructor( @ColorRes private fun getColorFromRoomId(roomId: String?): Int { - return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) { + return when ((roomId?.toList()?.sumOf { it.code } ?: 0) % 3) { 1 -> R.color.riotx_avatar_fill_2 2 -> R.color.riotx_avatar_fill_3 else -> R.color.riotx_avatar_fill_1 diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt index b549e01551..f067cd7599 100644 --- a/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt +++ b/vector/src/main/java/im/vector/app/features/media/AttachmentProviderFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -30,24 +31,31 @@ class AttachmentProviderFactory @Inject constructor( private val session: Session ) { - fun createProvider(attachments: List): RoomEventsAttachmentProvider { + fun createProvider(attachments: List, + coroutineScope: CoroutineScope + ): RoomEventsAttachmentProvider { return RoomEventsAttachmentProvider( - attachments, - imageContentRenderer, - vectorDateFormatter, - session.fileService(), - stringProvider + attachments = attachments, + imageContentRenderer = imageContentRenderer, + dateFormatter = vectorDateFormatter, + fileService = session.fileService(), + coroutineScope = coroutineScope, + stringProvider = stringProvider ) } - fun createProvider(attachments: List, room: Room?): DataAttachmentRoomProvider { + fun createProvider(attachments: List, + room: Room?, + coroutineScope: CoroutineScope + ): DataAttachmentRoomProvider { return DataAttachmentRoomProvider( - attachments, - room, - imageContentRenderer, - vectorDateFormatter, - session.fileService(), - stringProvider + attachments = attachments, + room = room, + imageContentRenderer = imageContentRenderer, + dateFormatter = vectorDateFormatter, + fileService = session.fileService(), + coroutineScope = coroutineScope, + stringProvider = stringProvider ) } } diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index 53996171a5..ca469bfbcb 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -31,8 +31,8 @@ import im.vector.lib.attachmentviewer.AttachmentInfo import im.vector.lib.attachmentviewer.AttachmentSourceProvider import im.vector.lib.attachmentviewer.ImageLoaderTarget import im.vector.lib.attachmentviewer.VideoLoaderTarget +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.events.model.isVideoMessage @@ -44,6 +44,7 @@ abstract class BaseAttachmentProvider( private val attachments: List, private val imageContentRenderer: ImageContentRenderer, protected val fileService: FileService, + private val coroutineScope: CoroutineScope, private val dateFormatter: VectorDateFormatter, private val stringProvider: StringProvider ) : AttachmentSourceProvider { @@ -155,7 +156,7 @@ abstract class BaseAttachmentProvider( target.onVideoURLReady(info.uid, data.url) } else { target.onVideoFileLoading(info.uid) - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.IO) { val result = runCatching { fileService.downloadFile( fileName = data.filename, @@ -178,5 +179,5 @@ abstract class BaseAttachmentProvider( // TODO("Not yet implemented") } - abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit)) + abstract suspend fun getFileForSharing(position: Int): File? } diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 630433506f..31162f309f 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -19,10 +19,8 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -35,8 +33,16 @@ class DataAttachmentRoomProvider( imageContentRenderer: ImageContentRenderer, dateFormatter: VectorDateFormatter, fileService: FileService, + coroutineScope: CoroutineScope, stringProvider: StringProvider -) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { +) : BaseAttachmentProvider( + attachments = attachments, + imageContentRenderer = imageContentRenderer, + fileService = fileService, + coroutineScope = coroutineScope, + dateFormatter = dateFormatter, + stringProvider = stringProvider +) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return getItem(position).let { @@ -78,20 +84,17 @@ class DataAttachmentRoomProvider( return room?.getTimeLineEvent(item.eventId) } - override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - val item = getItem(position) - GlobalScope.launch { - val result = runCatching { - fileService.downloadFile( - fileName = item.filename, - mimeType = item.mimeType, - url = item.url, - elementToDecrypt = item.elementToDecrypt - ) - } - withContext(Dispatchers.Main) { - callback(result.getOrNull()) - } - } + override suspend fun getFileForSharing(position: Int): File? { + return getItem(position) + .let { item -> + tryOrNull { + fileService.downloadFile( + fileName = item.filename, + mimeType = item.mimeType, + url = item.url, + elementToDecrypt = item.elementToDecrypt + ) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index fc6d5a1f22..1e0a3a2ad9 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -19,10 +19,8 @@ package im.vector.app.features.media import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.lib.attachmentviewer.AttachmentInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -41,8 +39,16 @@ class RoomEventsAttachmentProvider( imageContentRenderer: ImageContentRenderer, dateFormatter: VectorDateFormatter, fileService: FileService, + coroutineScope: CoroutineScope, stringProvider: StringProvider -) : BaseAttachmentProvider(attachments, imageContentRenderer, fileService, dateFormatter, stringProvider) { +) : BaseAttachmentProvider( + attachments = attachments, + imageContentRenderer = imageContentRenderer, + fileService = fileService, + coroutineScope = coroutineScope, + dateFormatter = dateFormatter, + stringProvider = stringProvider +) { override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return getItem(position).let { @@ -121,24 +127,19 @@ class RoomEventsAttachmentProvider( return getItem(position) } - override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { - getItem(position).let { timelineEvent -> - - val messageContent = timelineEvent.root.getClearContent().toModel() - as? MessageWithAttachmentContent - ?: return@let - GlobalScope.launch { - val result = runCatching { - fileService.downloadFile( - fileName = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + override suspend fun getFileForSharing(position: Int): File? { + return getItem(position) + .let { timelineEvent -> + timelineEvent.root.getClearContent().toModel() as? MessageWithAttachmentContent } - withContext(Dispatchers.Main) { - callback(result.getOrNull()) + ?.let { messageContent -> + tryOrNull { + fileService.downloadFile( + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + } } - } - } } } diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index c632a008ce..bc3acf3eec 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -28,7 +28,7 @@ import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.transition.Transition import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -42,6 +42,9 @@ import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ThemeUtils import im.vector.lib.attachmentviewer.AttachmentCommands import im.vector.lib.attachmentviewer.AttachmentViewerActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import timber.log.Timber import javax.inject.Inject @@ -119,11 +122,11 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA) val sourceProvider = if (inMemoryData != null) { initialIndex = inMemoryData.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) - dataSourceFactory.createProvider(inMemoryData, room) + dataSourceFactory.createProvider(inMemoryData, room, lifecycleScope) } else { val events = room?.getAttachmentMessages().orEmpty() initialIndex = events.indexOfFirst { it.eventId == args.eventId }.coerceAtLeast(0) - dataSourceFactory.createProvider(events) + dataSourceFactory.createProvider(events, lifecycleScope) } sourceProvider.interactionListener = this setSourceProvider(sourceProvider) @@ -264,9 +267,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen } override fun onShareTapped() { - currentSourceProvider?.getFileForSharing(currentPosition) { data -> - if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + lifecycleScope.launch(Dispatchers.IO) { + val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch + + withContext(Dispatchers.Main) { + shareMedia( + this@VectorAttachmentViewerActivity, + file, + getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri()) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index 80d2d8ba45..635de2ba16 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -25,8 +25,9 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.files.LocalFilesHelper +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -39,6 +40,9 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc private val activeSessionHolder: ActiveSessionHolder, private val errorFormatter: ErrorFormatter) { + private val sessionScope: CoroutineScope + get() = activeSessionHolder.getActiveSession().coroutineScope + @Parcelize data class Data( override val eventId: String, @@ -76,7 +80,7 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - GlobalScope.launch { + sessionScope.launch { val result = runCatching { activeSessionHolder.getActiveSession().fileService() .downloadFile( @@ -119,7 +123,7 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc thumbnailView.isVisible = true loadingView.isVisible = true - GlobalScope.launch { + sessionScope.launch { val result = runCatching { activeSessionHolder.getActiveSession().fileService() .downloadFile( diff --git a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt index adc618d82e..9c55b88805 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt @@ -60,6 +60,7 @@ class PinLocker @Inject constructor( return liveState } + @Suppress("EXPERIMENTAL_API_USAGE") private fun computeState() { GlobalScope.launch { val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt index 304720dfb0..2f8e013f46 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt @@ -88,6 +88,7 @@ class VectorFileLogger @Inject constructor( } } + @Suppress("EXPERIMENTAL_API_USAGE") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { fileHandler ?: return GlobalScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt index 095e250602..51dc62af8b 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt @@ -19,6 +19,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.EmojiChooserFragmentBinding @@ -51,6 +52,8 @@ class EmojiChooserFragment @Inject constructor( } } + override fun getCoroutineScope() = lifecycleScope + override fun firstVisibleSectionChange(section: Int) { viewModel.setCurrentSection(section) } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 92bc21be25..45d26e81eb 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -31,8 +31,8 @@ import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.abs @@ -221,7 +221,7 @@ class EmojiRecyclerAdapter @Inject constructor( } override fun getItemCount() = dataSource.rawData.categories - .sumBy { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } + .sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract fun bind(s: String?) @@ -278,6 +278,7 @@ class EmojiRecyclerAdapter @Inject constructor( } interface InteractionListener { + fun getCoroutineScope(): CoroutineScope fun firstVisibleSectionChange(section: Int) } @@ -323,11 +324,11 @@ class EmojiRecyclerAdapter @Inject constructor( // Log.i("SCROLL SPEED","scroll speed $dy") isFastScroll = abs(dy) > 50 val visible = (recyclerView.layoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() - GlobalScope.launch { + interactionListener?.getCoroutineScope()?.launch { val section = getSectionForAbsoluteIndex(visible) if (section != currentFirstVisibleSection) { currentFirstVisibleSection = section - GlobalScope.launch(Dispatchers.Main) { + interactionListener?.getCoroutineScope()?.launch(Dispatchers.Main) { interactionListener?.firstVisibleSectionChange(currentFirstVisibleSection) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 3867485e6f..2141b6bf27 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -36,6 +37,7 @@ import im.vector.app.databinding.FragmentRoomUploadsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -76,13 +78,21 @@ class RoomUploadsFragment @Inject constructor( shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri())) } is RoomUploadsViewEvents.FileReadyForSaving -> { - saveMedia( - context = requireContext(), - file = it.file, - title = it.title, - mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), - notificationUtils = notificationUtils - ) + lifecycleScope.launch { + runCatching { + saveMedia( + context = requireContext(), + file = it.file, + title = it.title, + mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), + notificationUtils = notificationUtils + ) + }.onFailure { failure -> + if (!isAdded) return@onFailure + showErrorInSnackbar(failure) + } + } + Unit } is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) }.exhaustive diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt index cff4ca0cb9..f558ba28c6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt @@ -181,7 +181,7 @@ object VectorLocale { } } // sort by human display names - .sortedBy { localeToLocalisedString(it).toLowerCase(it) } + .sortedBy { localeToLocalisedString(it).lowercase(it) } supportedLocales.clear() supportedLocales.addAll(list) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index 334464e304..adab8f8630 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -52,7 +52,6 @@ import im.vector.app.features.MainActivityArgs import im.vector.app.features.workers.signout.SignOutUiWorker import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -224,7 +223,7 @@ class VectorSettingsGeneralFragment @Inject constructor( it.summary = TextUtils.formatFileSize(requireContext(), size.toLong()) it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch(Dispatchers.Main) { // On UI Thread displayLoadingView() diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index 9654eb2190..effb593add 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -24,6 +24,7 @@ import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.profiles.profileSectionItem import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.safeCapitalize import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import java.util.Locale @@ -46,7 +47,7 @@ class LocalePickerController @Inject constructor( } localeItem { id(data.currentLocale.toString()) - title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) + title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale)) if (vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) } @@ -75,7 +76,7 @@ class LocalePickerController @Inject constructor( .forEach { localeItem { id(it.toString()) - title(VectorLocale.localeToLocalisedString(it).capitalize(it)) + title(VectorLocale.localeToLocalisedString(it).safeCapitalize(it)) if (vectorPreferences.developerMode()) { subtitle(VectorLocale.localeToLocalisedStringInfo(it)) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 1586b16ff6..01a0ba4b56 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -36,11 +36,11 @@ import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.ReportType import im.vector.app.features.roomprofile.RoomProfileActivity +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.events.model.EventType @@ -140,7 +140,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - GlobalScope.launch { + session.coroutineScope.launch { try { session.getRoom(spaceArgs.spaceId)?.leave(null) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 9fa04aabbb..f9acfb3ce6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -22,7 +22,7 @@ import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.resources.StringProvider -import kotlinx.coroutines.GlobalScope +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue @@ -465,7 +465,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo } private fun launchWidgetAPIAction(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict, block: suspend () -> Unit): Job { - return GlobalScope.launch { + // We should probably use a scope tight to the lifecycle here... + return session.coroutineScope.launch { kotlin.runCatching { block() }.fold( diff --git a/vector/src/main/res/values/font_certs.xml b/vector/src/main/res/values/font_certs.xml index 141bfc01d9..5ce2ca424d 100644 --- a/vector/src/main/res/values/font_certs.xml +++ b/vector/src/main/res/values/font_certs.xml @@ -1,17 +1,17 @@ - + @array/com_google_android_gms_fonts_certs_dev @array/com_google_android_gms_fonts_certs_prod - + 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 - \ No newline at end of file +