diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index 5cd8b55158..b55c39f41b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -281,7 +281,11 @@ class ElementCallActivity : @RequiresApi(Build.VERSION_CODES.O) override fun enterPipMode(): Boolean { - return enterPictureInPictureMode(getPictureInPictureParams()) + return if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + enterPictureInPictureMode(getPictureInPictureParams()) + } else { + false + } } @RequiresApi(Build.VERSION_CODES.O) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt index a288309422..3ab00a71bc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.voicemessages.VoiceMessageExcep import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.di.RoomScope import io.element.android.libraries.ui.utils.time.formatShort import io.element.android.services.analytics.api.AnalyticsService @@ -126,8 +127,8 @@ class VoiceMessagePresenter @AssistedInject constructor( it }, ) { - player.prepare().apply { - player.play() + player.prepare().flatMap { + runCatching { player.play() } } } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 1c213dd0ab..4a55b8549e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -71,7 +71,10 @@ fun Context.copyToClipboard( * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings */ -fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher? = null) { +fun Context.startNotificationSettingsIntent( + activityResultLauncher: ActivityResultLauncher? = null, + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), +) { val intent = Intent() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS @@ -85,10 +88,14 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu intent.data = Uri.fromParts("package", packageName, null) } - if (activityResultLauncher != null) { - activityResultLauncher.launch(intent) - } else { - startActivity(intent) + try { + if (activityResultLauncher != null) { + activityResultLauncher.launch(intent) + } else { + startActivity(intent) + } + } catch (activityNotFoundException: ActivityNotFoundException) { + toast(noActivityFoundMessage) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index b871062f70..5f9b00da35 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -68,7 +68,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener @@ -104,10 +103,12 @@ class RustMatrixRoom( override val roomId = RoomId(innerRoom.id()) override val roomInfoFlow: Flow = mxCallbackFlow { - launch { - val initial = innerRoom.roomInfo().let(matrixRoomInfoMapper::map) - channel.trySend(initial) - } + runCatching { innerRoom.roomInfo() } + .getOrNull() + ?.let(matrixRoomInfoMapper::map) + ?.let { initial -> + channel.trySend(initial) + } innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { override fun call(roomInfo: RoomInfo) { channel.trySend(matrixRoomInfoMapper.map(roomInfo)) @@ -116,10 +117,8 @@ class RustMatrixRoom( } override val roomTypingMembersFlow: Flow> = mxCallbackFlow { - launch { - val initial = emptyList() - channel.trySend(initial) - } + val initial = emptyList() + channel.trySend(initial) innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { override fun call(typingUserIds: List) { channel.trySend( @@ -625,9 +624,13 @@ class RustMatrixRoom( innerRoom.sendCallNotificationIfNeeded() } - override suspend fun setSendQueueEnabled(enabled: Boolean) = withContext(roomDispatcher) { - Timber.d("setSendQueuesEnabled: $enabled") - innerRoom.enableSendQueue(enabled) + override suspend fun setSendQueueEnabled(enabled: Boolean) { + withContext(roomDispatcher) { + Timber.d("setSendQueuesEnabled: $enabled") + runCatching { + innerRoom.enableSendQueue(enabled) + } + } } override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result = runCatching { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 334e42e266..46ef84ef73 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -97,9 +97,7 @@ internal fun RoomListServiceInterface.stateFlow(): Flow = trySendBlocking(state) } } - tryOrNull { - state(listener) - } + state(listener) }.buffer(Channel.UNLIMITED) internal fun RoomListServiceInterface.syncIndicator(): Flow = @@ -109,13 +107,11 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow = trySendBlocking(state) } } - tryOrNull { - state(listener) - } + state(listener) }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt index f071589078..a12a7c606b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import org.matrix.rustcomponents.sdk.TaskHandle -internal fun mxCallbackFlow(block: suspend ProducerScope.() -> TaskHandle?) = +internal fun mxCallbackFlow(block: suspend ProducerScope.() -> TaskHandle) = callbackFlow { val taskHandle: TaskHandle? = tryOrNull { block(this) diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerLauncher.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerLauncher.kt index 719ee29651..0c47059ec1 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerLauncher.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerLauncher.kt @@ -7,7 +7,9 @@ package io.element.android.libraries.mediapickers.api +import android.content.ActivityNotFoundException import androidx.activity.compose.ManagedActivityResultLauncher +import timber.log.Timber /** * Wrapper around [ManagedActivityResultLauncher] to be used with media/file pickers. @@ -25,11 +27,19 @@ class ComposePickerLauncher( private val defaultRequest: Input, ) : PickerLauncher { override fun launch() { - managedLauncher.launch(defaultRequest) + try { + managedLauncher.launch(defaultRequest) + } catch (activityNotFoundException: ActivityNotFoundException) { + Timber.w(activityNotFoundException, "No activity found") + } } override fun launch(customInput: Input) { - managedLauncher.launch(customInput) + try { + managedLauncher.launch(customInput) + } catch (activityNotFoundException: ActivityNotFoundException) { + Timber.w(activityNotFoundException, "No activity found") + } } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt index b0b6fcb307..255822b853 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt @@ -9,6 +9,9 @@ package io.element.android.libraries.mediaviewer.api.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -25,20 +28,30 @@ class PdfRendererManager( ) { private val mutex = Mutex() private var pdfRenderer: PdfRenderer? = null - private val mutablePdfPages = MutableStateFlow>(emptyList()) - val pdfPages: StateFlow> = mutablePdfPages + private val mutablePdfPages = MutableStateFlow>>(AsyncData.Uninitialized) + val pdfPages: StateFlow>> = mutablePdfPages fun open() { coroutineScope.launch { mutex.withLock { withContext(Dispatchers.IO) { - pdfRenderer = PdfRenderer(parcelFileDescriptor).apply { - // Preload just 3 pages so we can render faster - val firstPages = loadPages(from = 0, to = 3) - mutablePdfPages.value = firstPages - val nextPages = loadPages(from = 3, to = pageCount) - mutablePdfPages.value = firstPages + nextPages - } + pdfRenderer = runCatching { + PdfRenderer(parcelFileDescriptor) + }.fold( + onSuccess = { pdfRenderer -> + pdfRenderer.apply { + // Preload just 3 pages so we can render faster + val firstPages = loadPages(from = 0, to = 3) + mutablePdfPages.value = AsyncData.Success(firstPages.toImmutableList()) + val nextPages = loadPages(from = 3, to = pageCount) + mutablePdfPages.value = AsyncData.Success((firstPages + nextPages).toImmutableList()) + } + }, + onFailure = { + mutablePdfPages.value = AsyncData.Failure(it) + null + } + ) } } } @@ -47,7 +60,7 @@ class PdfRendererManager( fun close() { coroutineScope.launch { mutex.withLock { - mutablePdfPages.value.forEach { pdfPage -> + mutablePdfPages.value.dataOrNull()?.forEach { pdfPage -> pdfPage.close() } pdfRenderer?.close() diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt index 014efdad80..6202db88b1 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt @@ -28,13 +28,19 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.roundToPx import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import me.saket.telephoto.zoomable.zoomable +import java.io.IOException @Composable fun PdfViewer( @@ -59,7 +65,7 @@ fun PdfViewer( } val pdfPages = pdfViewerState.getPages() PdfPagesView( - pdfPages = pdfPages.toImmutableList(), + pdfPages = pdfPages, lazyListState = pdfViewerState.lazyListState, ) } @@ -67,6 +73,48 @@ fun PdfViewer( @Composable private fun PdfPagesView( + pdfPages: AsyncData>, + lazyListState: LazyListState, + modifier: Modifier = Modifier, +) { + when (pdfPages) { + is AsyncData.Uninitialized, + is AsyncData.Loading -> Unit + is AsyncData.Failure -> PdfPagesErrorView( + pdfPages.error, + modifier, + ) + is AsyncData.Success -> PdfPagesContentView( + pdfPages = pdfPages.data, + lazyListState = lazyListState, + modifier = modifier + ) + } +} + +@Composable +private fun PdfPagesErrorView( + error: Throwable, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = buildString { + append(stringResource(id = CommonStrings.error_unknown)) + append("\n\n") + append(error.localizedMessage) + }, + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyLgRegular, + ) + } +} + +@Composable +private fun PdfPagesContentView( pdfPages: ImmutableList, lazyListState: LazyListState, modifier: Modifier = Modifier, @@ -117,3 +165,11 @@ private fun PdfPageView( } } } + +@PreviewsDayNight +@Composable +internal fun PdfPagesErrorViewPreview() = ElementPreview { + PdfPagesErrorView( + error = IOException("file not in PDF format or corrupted"), + ) +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt index c27b36554a..72eb73c301 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt @@ -19,6 +19,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import me.saket.telephoto.zoomable.ZoomableState import me.saket.telephoto.zoomable.rememberZoomableState @@ -35,10 +37,10 @@ class PdfViewerState( private var pdfRendererManager by mutableStateOf(null) @Composable - fun getPages(): List { + fun getPages(): AsyncData> { return pdfRendererManager?.run { pdfPages.collectAsState().value - } ?: emptyList() + } ?: AsyncData.Uninitialized } fun openForWidth(maxWidth: Int) { diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_en.png new file mode 100644 index 0000000000..a038c331fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb0e18f25f8cd978830eb2a4db0cbef76376cdd34c025dc5e21dca39f0b8bd25 +size 13315 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Night_0_en.png new file mode 100644 index 0000000000..d07665658b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:336450bda0f5438f30b14fdb9d3a1d95ff9fe3a9ff325c4f860c50dbf6e7256b +size 13056