diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index fc3fba9c59a..9d5ed6ff8c1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.Overlay +import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient @@ -66,8 +67,8 @@ import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.services.analytics.api.AnalyticsService @@ -86,6 +87,7 @@ class MessagesFlowNode @AssistedInject constructor( private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint, + private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val analyticsService: AnalyticsService, private val room: MatrixRoom, private val roomMemberProfilesCache: RoomMemberProfilesCache, @@ -228,14 +230,22 @@ class MessagesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(callback, inputs)) } is NavTarget.MediaViewer -> { - val inputs = MediaViewerNode.Inputs( + val params = MediaViewerEntryPoint.Params( mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, thumbnailSource = navTarget.thumbnailSource, canDownload = true, canShare = true, ) - createNode(buildContext, listOf(inputs)) + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() { + overlay.hide() + } + } + mediaViewerEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() } is NavTarget.AttachmentPreview -> { val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index 2417d8346ea..e89d9a50524 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -20,12 +20,14 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer @ContributesNode(RoomScope::class) class AttachmentsPreviewNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: AttachmentsPreviewPresenter.Factory, + private val localMediaRenderer: LocalMediaRenderer, ) : Node(buildContext, plugins = plugins) { data class Inputs(val attachment: Attachment) : NodeInputs @@ -46,6 +48,7 @@ class AttachmentsPreviewNode @AssistedInject constructor( val state = presenter.present() AttachmentsPreviewView( state = state, + localMediaRenderer = localMediaRenderer, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index f7e215fa222..1a52316735c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -10,12 +10,9 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown @@ -23,9 +20,6 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider get() = sequenceOf( anAttachmentsPreviewState(), - anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()), - anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()), - anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)), anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 21450922820..876f6f9bdf2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize @@ -20,6 +21,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -34,20 +36,20 @@ import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.mediaviewer.api.local.LocalMediaView -import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer import io.element.android.libraries.textcomposer.TextComposer import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.display.TextDisplay -import me.saket.telephoto.zoomable.ZoomSpec -import me.saket.telephoto.zoomable.rememberZoomableState @OptIn(ExperimentalMaterial3Api::class) @Composable fun AttachmentsPreviewView( state: AttachmentsPreviewState, + localMediaRenderer: LocalMediaRenderer, modifier: Modifier = Modifier, ) { fun postSendAttachment() { @@ -82,6 +84,7 @@ fun AttachmentsPreviewView( ) { AttachmentPreviewContent( state = state, + localMediaRenderer = localMediaRenderer, onSendClick = ::postSendAttachment, ) } @@ -129,6 +132,7 @@ private fun AttachmentSendStateView( @Composable private fun AttachmentPreviewContent( state: AttachmentsPreviewState, + localMediaRenderer: LocalMediaRenderer, onSendClick: () -> Unit, ) { Box( @@ -142,17 +146,7 @@ private fun AttachmentPreviewContent( ) { when (val attachment = state.attachment) { is Attachment.Media -> { - val localMediaViewState = rememberLocalMediaViewState( - zoomableState = rememberZoomableState( - zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) - ) - ) - LocalMediaView( - modifier = Modifier.fillMaxSize(), - localMedia = attachment.localMedia, - localMediaViewState = localMediaViewState, - onClick = {} - ) + localMediaRenderer.Render(attachment.localMedia) } } } @@ -205,5 +199,15 @@ private fun AttachmentsPreviewBottomActions( internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark { AttachmentsPreviewView( state = state, + localMediaRenderer = object : LocalMediaRenderer { + @Composable + override fun Render(localMedia: LocalMedia) { + Image( + painter = painterResource(id = CommonDrawables.sample_background), + modifier = Modifier.fillMaxSize(), + contentDescription = null, + ) + } + } ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index 405d356edf0..51c4cb43ba0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -35,7 +35,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index 07ff31690e6..b771141ce33 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH -import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 4030e302721..166a22d694e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -32,20 +33,16 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper -import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.operation.show -import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.parcelize.Parcelize @@ -59,6 +56,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( private val room: MatrixRoom, private val analyticsService: AnalyticsService, private val messagesEntryPoint: MessagesEntryPoint, + private val mediaViewerEntryPoint: MediaViewerEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -202,22 +200,18 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext, plugins) } is NavTarget.AvatarPreview -> { - // We need to fake the MimeType here for the viewer to work. - val mimeType = MimeTypes.Images - val input = MediaViewerNode.Inputs( - mediaInfo = MediaInfo( - filename = navTarget.name, - caption = null, - mimeType = mimeType, - formattedFileSize = "", - fileExtension = "" - ), - mediaSource = MediaSource(url = navTarget.avatarUrl), - thumbnailSource = null, - canDownload = false, - canShare = false, - ) - createNode(buildContext, listOf(input)) + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + } + mediaViewerEntryPoint.nodeBuilder(this, buildContext) + .avatar( + navTarget.name, + navTarget.avatarUrl, + ) + .callback(callback) + .build() } is NavTarget.PollHistory -> { diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index b544ad4750b..ce0d4a07f0a 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -24,18 +25,14 @@ import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.impl.root.UserProfileNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper -import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -44,6 +41,7 @@ class UserProfileFlowNode @AssistedInject constructor( @Assisted plugins: List, private val elementCallEntryPoint: ElementCallEntryPoint, private val sessionIdHolder: CurrentSessionIdHolder, + private val mediaViewerEntryPoint: MediaViewerEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -80,22 +78,18 @@ class UserProfileFlowNode @AssistedInject constructor( createNode(buildContext, listOf(callback, params)) } is NavTarget.AvatarPreview -> { - // We need to fake the MimeType here for the viewer to work. - val mimeType = MimeTypes.Images - val input = MediaViewerNode.Inputs( - mediaInfo = MediaInfo( + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + } + mediaViewerEntryPoint.nodeBuilder(this, buildContext) + .avatar( filename = navTarget.name, - caption = null, - mimeType = mimeType, - formattedFileSize = "", - fileExtension = "", - ), - mediaSource = MediaSource(url = navTarget.avatarUrl), - thumbnailSource = null, - canDownload = false, - canShare = false, - ) - createNode(buildContext, listOf(input)) + avatarUrl = navTarget.avatarUrl + ) + .callback(callback) + .build() } } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/avatar/AvatarPreviewNode.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/avatar/AvatarPreviewNode.kt deleted file mode 100644 index 159b7f56f67..00000000000 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/avatar/AvatarPreviewNode.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.userprofile.shared.avatar - -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter - -@ContributesNode(SessionScope::class) -class AvatarPreviewNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: MediaViewerPresenter.Factory, -) : MediaViewerNode(buildContext, plugins, presenterFactory) diff --git a/libraries/mediaviewer/api/build.gradle.kts b/libraries/mediaviewer/api/build.gradle.kts index 5d434ae0946..d36a3605d2d 100644 --- a/libraries/mediaviewer/api/build.gradle.kts +++ b/libraries/mediaviewer/api/build.gradle.kts @@ -13,45 +13,12 @@ plugins { android { namespace = "io.element.android.libraries.mediaviewer.api" - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } } setupAnvil() dependencies { - implementation(libs.coil.compose) - implementation(libs.androidx.media3.exoplayer) - implementation(libs.androidx.media3.ui) - implementation(libs.coroutines.core) - implementation(libs.dagger) - implementation(libs.telephoto.zoomableimage) - implementation(libs.vanniktech.blurhash) - implementation(libs.telephoto.flick) - - implementation(projects.libraries.androidutils) - implementation(projects.libraries.architecture) implementation(projects.libraries.core) - implementation(projects.libraries.dateformatter.api) - implementation(projects.libraries.di) - implementation(projects.libraries.designsystem) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrixui) - implementation(projects.libraries.uiStrings) - - testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.mediaviewer.test) - testImplementation(projects.tests.testutils) - testImplementation(libs.test.junit) - testImplementation(libs.test.truth) - testImplementation(libs.test.mockk) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.turbine) - testImplementation(libs.coroutines.core) - testImplementation(libs.coroutines.test) - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt similarity index 93% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt index 5ded65b2b34..93cd2f9378f 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt @@ -1,11 +1,11 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2024 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local +package io.element.android.libraries.mediaviewer.api import android.os.Parcelable import io.element.android.libraries.core.mimetype.MimeTypes diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt new file mode 100644 index 00000000000..fb5ee5dece7 --- /dev/null +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.media.MediaSource + +interface MediaViewerEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun params(params: Params): NodeBuilder + fun avatar(filename: String, avatarUrl: String): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onDone() + } + + data class Params( + val mediaInfo: MediaInfo, + val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, + val canDownload: Boolean, + val canShare: Boolean, + ) : NodeInputs +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt index 56bda259a69..fade6706977 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.api.local import android.net.Uri import android.os.Parcelable import androidx.compose.runtime.Immutable +import io.element.android.libraries.mediaviewer.api.MediaInfo import kotlinx.parcelize.Parcelize @Parcelize diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt index 92000702636..beeccb7c488 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.api.local import android.net.Uri import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.mediaviewer.api.MediaInfo interface LocalMediaFactory { /** diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt new file mode 100644 index 00000000000..3b387ef0d77 --- /dev/null +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.api.local + +import androidx.compose.runtime.Composable + +interface LocalMediaRenderer { + @Composable + fun Render(localMedia: LocalMedia) +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt deleted file mode 100644 index b7102b0f845..00000000000 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.mediaviewer.api.local - -import android.annotation.SuppressLint -import android.net.Uri -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.widget.FrameLayout -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.Timeline -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.PlayerView -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.designsystem.utils.KeepScreenOn -import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize -import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWrapper -import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer -import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState -import io.element.android.libraries.mediaviewer.api.player.MediaPlayerControllerState -import io.element.android.libraries.mediaviewer.api.player.MediaPlayerControllerView -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.coroutines.delay -import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage -import me.saket.telephoto.zoomable.rememberZoomableImageState -import kotlin.time.Duration.Companion.seconds - -@Composable -fun LocalMediaView( - localMedia: LocalMedia?, - onClick: () -> Unit, - modifier: Modifier = Modifier, - localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), - mediaInfo: MediaInfo? = localMedia?.info, -) { - val mimeType = mediaInfo?.mimeType - when { - mimeType.isMimeTypeImage() -> MediaImageView( - localMediaViewState = localMediaViewState, - localMedia = localMedia, - modifier = modifier, - onClick = onClick, - ) - mimeType.isMimeTypeVideo() -> MediaVideoView( - localMediaViewState = localMediaViewState, - localMedia = localMedia, - modifier = modifier, - ) - mimeType == MimeTypes.Pdf -> MediaPDFView( - localMediaViewState = localMediaViewState, - localMedia = localMedia, - modifier = modifier, - onClick = onClick, - ) - // TODO handle audio with exoplayer - else -> MediaFileView( - localMediaViewState = localMediaViewState, - uri = localMedia?.uri, - info = mediaInfo, - modifier = modifier, - onClick = onClick, - ) - } -} - -@Composable -private fun MediaImageView( - localMediaViewState: LocalMediaViewState, - localMedia: LocalMedia?, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - if (LocalInspectionMode.current) { - Image( - painter = painterResource(id = CommonDrawables.sample_background), - modifier = modifier, - contentDescription = null, - ) - } else { - val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) - localMediaViewState.isReady = zoomableImageState.isImageDisplayed - ZoomableAsyncImage( - modifier = modifier, - state = zoomableImageState, - model = localMedia?.uri, - contentDescription = stringResource(id = CommonStrings.common_image), - contentScale = ContentScale.Fit, - onClick = { onClick() } - ) - } -} - -@Composable -private fun MediaVideoView( - localMediaViewState: LocalMediaViewState, - localMedia: LocalMedia?, - modifier: Modifier = Modifier, -) { - if (LocalInspectionMode.current) { - Text( - modifier = modifier - .background(ElementTheme.colors.bgSubtlePrimary) - .wrapContentSize(), - text = "A Video Player will render here", - ) - } else { - ExoPlayerMediaVideoView( - localMediaViewState = localMediaViewState, - localMedia = localMedia, - modifier = modifier, - ) - } -} - -@SuppressLint("UnsafeOptInUsageError") -@Composable -private fun ExoPlayerMediaVideoView( - localMediaViewState: LocalMediaViewState, - localMedia: LocalMedia?, - modifier: Modifier = Modifier, -) { - var mediaPlayerControllerState: MediaPlayerControllerState by remember { - mutableStateOf( - MediaPlayerControllerState( - isVisible = false, - isPlaying = false, - progressInMillis = 0, - durationInMillis = 0, - isMuted = false, - ) - ) - } - - val playableState: PlayableState.Playable by remember { - derivedStateOf { - PlayableState.Playable( - isShowingControls = mediaPlayerControllerState.isVisible, - ) - } - } - - localMediaViewState.playableState = playableState - - val context = LocalContext.current - val exoPlayer = remember { - ExoPlayerWrapper.create(context) - } - val playerListener = object : Player.Listener { - override fun onRenderedFirstFrame() { - localMediaViewState.isReady = true - } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - isPlaying = isPlaying, - ) - } - - override fun onVolumeChanged(volume: Float) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - isMuted = volume == 0f, - ) - } - - override fun onTimelineChanged(timeline: Timeline, reason: Int) { - if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - durationInMillis = exoPlayer.duration, - ) - } - } - } - - LaunchedEffect(Unit) { - exoPlayer.addListener(playerListener) - exoPlayer.prepare() - } - - var autoHideController by remember { mutableIntStateOf(0) } - - LaunchedEffect(autoHideController) { - delay(5.seconds) - if (exoPlayer.isPlaying) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - isVisible = false, - ) - } - } - - LaunchedEffect(exoPlayer.isPlaying) { - if (exoPlayer.isPlaying) { - while (true) { - mediaPlayerControllerState = mediaPlayerControllerState.copy( - progressInMillis = exoPlayer.currentPosition, - ) - delay(200) - } - } else { - // Ensure we render the final state - mediaPlayerControllerState = mediaPlayerControllerState.copy( - progressInMillis = exoPlayer.currentPosition, - ) - } - } - if (localMedia?.uri != null) { - LaunchedEffect(localMedia.uri) { - val mediaItem = MediaItem.fromUri(localMedia.uri) - exoPlayer.setMediaItem(mediaItem) - } - } else { - exoPlayer.setMediaItems(emptyList()) - } - KeepScreenOn(mediaPlayerControllerState.isPlaying) - Box( - modifier = modifier - .background(ElementTheme.colors.bgSubtlePrimary) - .wrapContentSize(), - ) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { - PlayerView(context).apply { - player = exoPlayer - resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT - layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - setOnClickListener { - autoHideController++ - mediaPlayerControllerState = mediaPlayerControllerState.copy( - isVisible = !mediaPlayerControllerState.isVisible, - ) - } - useController = false - } - }, - onRelease = { playerView -> - playerView.setOnClickListener(null) - playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?) - playerView.player = null - }, - ) - MediaPlayerControllerView( - state = mediaPlayerControllerState, - onTogglePlay = { - autoHideController++ - if (exoPlayer.isPlaying) { - exoPlayer.pause() - } else { - if (exoPlayer.playbackState == Player.STATE_ENDED) { - exoPlayer.seekTo(0) - } else { - exoPlayer.play() - } - } - }, - onSeekChange = { - autoHideController++ - if (exoPlayer.isPlaying.not()) { - exoPlayer.play() - } - exoPlayer.seekTo(it.toLong()) - }, - onToggleMute = { - autoHideController++ - exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f - }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - ) - } - - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> exoPlayer.play() - Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() - Lifecycle.Event.ON_DESTROY -> { - exoPlayer.release() - exoPlayer.removeListener(playerListener) - } - else -> Unit - } - } -} - -@Composable -private fun MediaPDFView( - localMediaViewState: LocalMediaViewState, - localMedia: LocalMedia?, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val pdfViewerState = rememberPdfViewerState( - model = localMedia?.uri, - zoomableState = localMediaViewState.zoomableState, - ) - localMediaViewState.isReady = pdfViewerState.isLoaded - PdfViewer( - pdfViewerState = pdfViewerState, - onClick = onClick, - modifier = modifier, - ) -} - -@Composable -private fun MediaFileView( - localMediaViewState: LocalMediaViewState, - uri: Uri?, - info: MediaInfo?, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() - localMediaViewState.isReady = uri != null - - val interactionSource = remember { MutableInteractionSource() } - Box( - modifier = modifier - .padding(horizontal = 8.dp) - .clickable( - onClick = onClick, - interactionSource = interactionSource, - indication = null - ), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(72.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.onBackground), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = if (isAudio) Icons.Outlined.GraphicEq else CompoundIcons.Attachment(), - contentDescription = null, - tint = MaterialTheme.colorScheme.background, - modifier = Modifier - .size(32.dp) - .rotate(if (isAudio) 0f else -45f), - ) - } - if (info != null) { - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = info.filename, - maxLines = 2, - style = ElementTheme.typography.fontBodyLgRegular, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = formatFileExtensionAndSize(info.fileExtension, info.formattedFileSize), - style = ElementTheme.typography.fontBodyMdRegular, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary - ) - } - } - } -} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt index 32ca151160b..e7586a6f877 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt @@ -7,30 +7,6 @@ package io.element.android.libraries.mediaviewer.api.util -import android.webkit.MimeTypeMap -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import javax.inject.Inject - interface FileExtensionExtractor { fun extractFromName(name: String): String } - -@ContributesBinding(AppScope::class) -class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor { - override fun extractFromName(name: String): String { - val fileExtension = name.substringAfterLast('.', "") - // Makes sure the extension is known by the system, otherwise default to binary extension. - return if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) { - fileExtension - } else { - "bin" - } - } -} - -class FileExtensionExtractorWithoutValidation : FileExtensionExtractor { - override fun extractFromName(name: String): String { - return name.substringAfterLast('.', "") - } -} diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index a814f64ab7d..5ebc252343d 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -13,6 +13,11 @@ plugins { android { namespace = "io.element.android.libraries.mediaviewer.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -21,6 +26,23 @@ dependencies { implementation(libs.coroutines.core) implementation(libs.dagger) + implementation(libs.coil.compose) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.telephoto.zoomableimage) + implementation(libs.vanniktech.blurhash) + implementation(libs.telephoto.flick) + + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.di) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.uiStrings) + api(projects.libraries.mediaviewer.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) @@ -39,4 +61,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.core) testImplementation(libs.coroutines.test) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt new file mode 100644 index 00000000000..5bd16c840b7 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaViewerEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : MediaViewerEntryPoint.NodeBuilder { + override fun callback(callback: MediaViewerEntryPoint.Callback): MediaViewerEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun params(params: MediaViewerEntryPoint.Params): MediaViewerEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun avatar(filename: String, avatarUrl: String): MediaViewerEntryPoint.NodeBuilder { + // We need to fake the MimeType here for the viewer to work. + val mimeType = MimeTypes.Images + return params( + MediaViewerEntryPoint.Params( + mediaInfo = MediaInfo( + filename = filename, + caption = null, + mimeType = mimeType, + formattedFileSize = "", + fileExtension = "" + ), + mediaSource = MediaSource(url = avatarUrl), + thumbnailSource = null, + canDownload = false, + canShare = false, + ) + ) + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt index 66bed465ed9..7bd92a800f0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt @@ -35,7 +35,6 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt index 06fefa26230..461563f4653 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt @@ -20,9 +20,9 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.toFile +import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory -import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import javax.inject.Inject diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt new file mode 100644 index 00000000000..6b1005dccf7 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer +import me.saket.telephoto.zoomable.ZoomSpec +import me.saket.telephoto.zoomable.rememberZoomableState +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer { + @Composable + override fun Render(localMedia: LocalMedia) { + val localMediaViewState = rememberLocalMediaViewState( + zoomableState = rememberZoomableState( + zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) + ) + ) + LocalMediaView( + modifier = Modifier.fillMaxSize(), + localMedia = localMedia, + localMediaViewState = localMediaViewState, + onClick = {} + ) + } +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaActions.kt similarity index 87% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaActions.kt index c7f0ba7d90a..92f873e2ea0 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaActions.kt @@ -5,9 +5,10 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local +package io.element.android.libraries.mediaviewer.impl.local import androidx.compose.runtime.Composable +import io.element.android.libraries.mediaviewer.api.local.LocalMedia interface LocalMediaActions { @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt new file mode 100644 index 00000000000..44392e27b9a --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView +import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView +import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView +import io.element.android.libraries.mediaviewer.impl.local.video.MediaVideoView + +@Composable +fun LocalMediaView( + localMedia: LocalMedia?, + onClick: () -> Unit, + modifier: Modifier = Modifier, + localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), + mediaInfo: MediaInfo? = localMedia?.info, +) { + val mimeType = mediaInfo?.mimeType + when { + mimeType.isMimeTypeImage() -> MediaImageView( + localMediaViewState = localMediaViewState, + localMedia = localMedia, + modifier = modifier, + onClick = onClick, + ) + mimeType.isMimeTypeVideo() -> MediaVideoView( + localMediaViewState = localMediaViewState, + localMedia = localMedia, + modifier = modifier, + ) + mimeType == MimeTypes.Pdf -> MediaPdfView( + localMediaViewState = localMediaViewState, + localMedia = localMedia, + modifier = modifier, + onClick = onClick, + ) + // TODO handle audio with exoplayer + else -> MediaFileView( + localMediaViewState = localMediaViewState, + uri = localMedia?.uri, + info = mediaInfo, + modifier = modifier, + onClick = onClick, + ) + } +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaViewState.kt similarity index 95% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaViewState.kt index b7237c26eb5..8705ef49d3b 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaViewState.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local +package io.element.android.libraries.mediaviewer.impl.local import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt new file mode 100644 index 00000000000..ec50ef9f4c1 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local.file + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.GraphicEq +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState +import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState + +@Composable +fun MediaFileView( + localMediaViewState: LocalMediaViewState, + uri: Uri?, + info: MediaInfo?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() + localMediaViewState.isReady = uri != null + + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .padding(horizontal = 8.dp) + .clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null + ), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(72.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onBackground), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = if (isAudio) Icons.Outlined.GraphicEq else CompoundIcons.Attachment(), + contentDescription = null, + tint = MaterialTheme.colorScheme.background, + modifier = Modifier + .size(32.dp) + .rotate(if (isAudio) 0f else -45f), + ) + } + if (info != null) { + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = info.filename, + maxLines = 2, + style = ElementTheme.typography.fontBodyLgRegular, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = formatFileExtensionAndSize(info.fileExtension, info.formattedFileSize), + style = ElementTheme.typography.fontBodyMdRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun MediaFileViewPreview( + @PreviewParameter(MediaInfoFileProvider::class) info: MediaInfo +) = ElementPreview { + MediaFileView( + modifier = Modifier.fillMaxSize(), + localMediaViewState = rememberLocalMediaViewState(), + uri = null, + info = info, + onClick = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt new file mode 100644 index 00000000000..980f9eba89a --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local.file + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo + +open class MediaInfoFileProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPdfMediaInfo(), + anAudioMediaInfo(), + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt new file mode 100644 index 00000000000..a286b7dc2d0 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local.image + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState +import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState +import io.element.android.libraries.ui.strings.CommonStrings +import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage +import me.saket.telephoto.zoomable.rememberZoomableImageState + +@Composable +fun MediaImageView( + localMediaViewState: LocalMediaViewState, + localMedia: LocalMedia?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(id = CommonDrawables.sample_background), + modifier = modifier, + contentDescription = null, + ) + } else { + val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) + localMediaViewState.isReady = zoomableImageState.isImageDisplayed + ZoomableAsyncImage( + modifier = modifier, + state = zoomableImageState, + model = localMedia?.uri, + contentDescription = stringResource(id = CommonStrings.common_image), + contentScale = ContentScale.Fit, + onClick = { onClick() } + ) + } +} + +@PreviewsDayNight +@Composable +internal fun MediaImageViewPreview() = ElementPreview { + MediaImageView( + modifier = Modifier.fillMaxSize(), + localMediaViewState = rememberLocalMediaViewState(), + localMedia = null, + onClick = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/MediaPdfView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/MediaPdfView.kt new file mode 100644 index 00000000000..f2694fb2775 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/MediaPdfView.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local.pdf + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState + +@Composable +fun MediaPdfView( + localMediaViewState: LocalMediaViewState, + localMedia: LocalMedia?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val pdfViewerState = rememberPdfViewerState( + model = localMedia?.uri, + zoomableState = localMediaViewState.zoomableState, + ) + localMediaViewState.isReady = pdfViewerState.isLoaded + PdfViewer( + pdfViewerState = pdfViewerState, + onClick = onClick, + modifier = modifier, + ) +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt similarity index 91% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt index e73f24ea618..bd931cadcb9 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.pdf +package io.element.android.libraries.mediaviewer.impl.local.pdf import android.content.Context import android.net.Uri diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfPage.kt similarity index 98% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfPage.kt index 137c3e321f8..655d577fda5 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfPage.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.pdf +package io.element.android.libraries.mediaviewer.impl.local.pdf import android.graphics.Bitmap import android.graphics.Canvas diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt similarity index 97% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt index 255822b8533..02f5ef58bf3 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.pdf +package io.element.android.libraries.mediaviewer.impl.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt similarity index 98% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt index 6202db88b19..ffb8e83d453 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.pdf +package io.element.android.libraries.mediaviewer.impl.local.pdf import androidx.compose.foundation.Image import androidx.compose.foundation.background diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewerState.kt similarity index 97% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewerState.kt index 72eb73c3015..095d4d15c4f 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewerState.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.pdf +package io.element.android.libraries.mediaviewer.impl.local.pdf import android.content.Context import androidx.compose.foundation.lazy.LazyListState diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt new file mode 100644 index 00000000000..c5176375767 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +@file:Suppress( + "OVERRIDE_DEPRECATION", + "RedundantNullableReturnType", + "DEPRECATION", +) + +package io.element.android.libraries.mediaviewer.impl.local.video + +import android.annotation.SuppressLint +import android.media.AudioDeviceInfo +import android.os.Looper +import android.view.Surface +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.TextureView +import androidx.media3.common.AudioAttributes +import androidx.media3.common.AuxEffectInfo +import androidx.media3.common.DeviceInfo +import androidx.media3.common.Effect +import androidx.media3.common.Format +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.PriorityTaskManager +import androidx.media3.common.Timeline +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Clock +import androidx.media3.common.util.Size +import androidx.media3.exoplayer.DecoderCounters +import androidx.media3.exoplayer.ExoPlaybackException +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.PlayerMessage +import androidx.media3.exoplayer.Renderer +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.analytics.AnalyticsCollector +import androidx.media3.exoplayer.analytics.AnalyticsListener +import androidx.media3.exoplayer.image.ImageOutput +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ShuffleOrder +import androidx.media3.exoplayer.source.TrackGroupArray +import androidx.media3.exoplayer.trackselection.TrackSelectionArray +import androidx.media3.exoplayer.trackselection.TrackSelector +import androidx.media3.exoplayer.video.VideoFrameMetadataListener +import androidx.media3.exoplayer.video.spherical.CameraMotionListener +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage + +@SuppressLint("UnsafeOptInUsageError") +@ExcludeFromCoverage +class ExoPlayerForPreview( + private val isPlaying: Boolean = false, +) : ExoPlayer { + override fun getApplicationLooper(): Looper = throw NotImplementedError() + override fun addListener(listener: Player.Listener) {} + override fun removeListener(listener: Player.Listener) {} + override fun setMediaItems(mediaItems: MutableList) {} + override fun setMediaItems(mediaItems: MutableList, resetPosition: Boolean) {} + override fun setMediaItems(mediaItems: MutableList, startIndex: Int, startPositionMs: Long) {} + override fun setMediaItem(mediaItem: MediaItem) {} + override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) {} + override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) {} + override fun addMediaItem(mediaItem: MediaItem) {} + override fun addMediaItem(index: Int, mediaItem: MediaItem) {} + override fun addMediaItems(mediaItems: MutableList) {} + override fun addMediaItems(index: Int, mediaItems: MutableList) {} + override fun moveMediaItem(currentIndex: Int, newIndex: Int) {} + override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) {} + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) {} + override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: MutableList) {} + override fun removeMediaItem(index: Int) {} + override fun removeMediaItems(fromIndex: Int, toIndex: Int) {} + override fun clearMediaItems() {} + override fun isCommandAvailable(command: Int): Boolean = throw NotImplementedError() + override fun canAdvertiseSession(): Boolean = throw NotImplementedError() + override fun getAvailableCommands(): Player.Commands = throw NotImplementedError() + override fun prepare(mediaSource: MediaSource) {} + override fun prepare(mediaSource: MediaSource, resetPosition: Boolean, resetState: Boolean) {} + override fun prepare() {} + override fun getPlaybackState(): Int = throw NotImplementedError() + override fun getPlaybackSuppressionReason(): Int = throw NotImplementedError() + override fun isPlaying() = isPlaying + override fun getPlayerError(): ExoPlaybackException? = null + override fun play() {} + override fun pause() {} + override fun setPlayWhenReady(playWhenReady: Boolean) {} + override fun getPlayWhenReady(): Boolean = throw NotImplementedError() + override fun setRepeatMode(repeatMode: Int) {} + override fun getRepeatMode(): Int = throw NotImplementedError() + override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} + override fun getShuffleModeEnabled(): Boolean = throw NotImplementedError() + override fun isLoading(): Boolean = throw NotImplementedError() + override fun seekToDefaultPosition() {} + override fun seekToDefaultPosition(mediaItemIndex: Int) {} + override fun seekTo(positionMs: Long) {} + override fun seekTo(mediaItemIndex: Int, positionMs: Long) {} + override fun getSeekBackIncrement(): Long = throw NotImplementedError() + override fun seekBack() {} + override fun getSeekForwardIncrement(): Long = throw NotImplementedError() + override fun seekForward() {} + override fun hasPreviousMediaItem(): Boolean = throw NotImplementedError() + override fun seekToPreviousWindow() {} + override fun seekToPreviousMediaItem() {} + override fun getMaxSeekToPreviousPosition(): Long = throw NotImplementedError() + override fun seekToPrevious() {} + override fun hasNext(): Boolean = throw NotImplementedError() + override fun hasNextWindow(): Boolean = throw NotImplementedError() + override fun hasNextMediaItem(): Boolean = throw NotImplementedError() + override fun next() {} + override fun seekToNextWindow() {} + override fun seekToNextMediaItem() {} + override fun seekToNext() {} + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {} + override fun setPlaybackSpeed(speed: Float) {} + override fun getPlaybackParameters(): PlaybackParameters = throw NotImplementedError() + override fun stop() {} + override fun release() {} + override fun getCurrentTracks(): Tracks = throw NotImplementedError() + override fun getTrackSelectionParameters(): TrackSelectionParameters = throw NotImplementedError() + override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {} + override fun getMediaMetadata(): MediaMetadata = throw NotImplementedError() + override fun getPlaylistMetadata(): MediaMetadata = throw NotImplementedError() + override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {} + override fun getCurrentManifest(): Any? = throw NotImplementedError() + override fun getCurrentTimeline(): Timeline = throw NotImplementedError() + override fun getCurrentPeriodIndex(): Int = throw NotImplementedError() + override fun getCurrentWindowIndex(): Int = throw NotImplementedError() + override fun getCurrentMediaItemIndex(): Int = throw NotImplementedError() + override fun getNextWindowIndex(): Int = throw NotImplementedError() + override fun getNextMediaItemIndex(): Int = throw NotImplementedError() + override fun getPreviousWindowIndex(): Int = throw NotImplementedError() + override fun getPreviousMediaItemIndex(): Int = throw NotImplementedError() + override fun getCurrentMediaItem(): MediaItem? = throw NotImplementedError() + override fun getMediaItemCount(): Int = throw NotImplementedError() + override fun getMediaItemAt(index: Int): MediaItem = throw NotImplementedError() + override fun getDuration(): Long = throw NotImplementedError() + override fun getCurrentPosition(): Long = throw NotImplementedError() + override fun getBufferedPosition(): Long = throw NotImplementedError() + override fun getBufferedPercentage(): Int = throw NotImplementedError() + override fun getTotalBufferedDuration(): Long = throw NotImplementedError() + override fun isCurrentWindowDynamic(): Boolean = throw NotImplementedError() + override fun isCurrentMediaItemDynamic(): Boolean = throw NotImplementedError() + override fun isCurrentWindowLive(): Boolean = throw NotImplementedError() + override fun isCurrentMediaItemLive(): Boolean = throw NotImplementedError() + override fun getCurrentLiveOffset(): Long = throw NotImplementedError() + override fun isCurrentWindowSeekable(): Boolean = throw NotImplementedError() + override fun isCurrentMediaItemSeekable(): Boolean = throw NotImplementedError() + override fun isPlayingAd(): Boolean = throw NotImplementedError() + override fun getCurrentAdGroupIndex(): Int = throw NotImplementedError() + override fun getCurrentAdIndexInAdGroup(): Int = throw NotImplementedError() + override fun getContentDuration(): Long = throw NotImplementedError() + override fun getContentPosition(): Long = throw NotImplementedError() + override fun getContentBufferedPosition(): Long = throw NotImplementedError() + override fun getAudioAttributes(): AudioAttributes = throw NotImplementedError() + override fun setVolume(volume: Float) = throw NotImplementedError() + override fun getVolume(): Float = throw NotImplementedError() + override fun clearVideoSurface() {} + override fun clearVideoSurface(surface: Surface?) {} + override fun setVideoSurface(surface: Surface?) {} + override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {} + override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {} + override fun setVideoSurfaceView(surfaceView: SurfaceView?) {} + override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {} + override fun setVideoTextureView(textureView: TextureView?) {} + override fun clearVideoTextureView(textureView: TextureView?) {} + override fun getVideoSize(): VideoSize = throw NotImplementedError() + override fun getSurfaceSize(): Size = throw NotImplementedError() + override fun getCurrentCues(): CueGroup = throw NotImplementedError() + override fun getDeviceInfo(): DeviceInfo = throw NotImplementedError() + override fun getDeviceVolume(): Int = throw NotImplementedError() + override fun isDeviceMuted(): Boolean = throw NotImplementedError() + override fun setDeviceVolume(volume: Int) {} + override fun setDeviceVolume(volume: Int, flags: Int) {} + override fun increaseDeviceVolume() {} + override fun increaseDeviceVolume(flags: Int) {} + override fun decreaseDeviceVolume() {} + override fun decreaseDeviceVolume(flags: Int) {} + override fun setDeviceMuted(muted: Boolean) {} + override fun setDeviceMuted(muted: Boolean, flags: Int) {} + override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) {} + override fun getAudioComponent(): ExoPlayer.AudioComponent? = throw NotImplementedError() + override fun getVideoComponent(): ExoPlayer.VideoComponent? = throw NotImplementedError() + override fun getTextComponent(): ExoPlayer.TextComponent? = throw NotImplementedError() + override fun getDeviceComponent(): ExoPlayer.DeviceComponent? = throw NotImplementedError() + override fun addAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {} + override fun removeAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {} + override fun getAnalyticsCollector(): AnalyticsCollector = throw NotImplementedError() + override fun addAnalyticsListener(listener: AnalyticsListener) {} + override fun removeAnalyticsListener(listener: AnalyticsListener) {} + override fun getRendererCount(): Int = throw NotImplementedError() + override fun getRendererType(index: Int): Int = throw NotImplementedError() + override fun getRenderer(index: Int): Renderer = throw NotImplementedError() + override fun getTrackSelector(): TrackSelector? = throw NotImplementedError() + override fun getCurrentTrackGroups(): TrackGroupArray = throw NotImplementedError() + override fun getCurrentTrackSelections(): TrackSelectionArray = throw NotImplementedError() + override fun getPlaybackLooper(): Looper = throw NotImplementedError() + override fun getClock(): Clock = throw NotImplementedError() + override fun setMediaSources(mediaSources: MutableList) {} + override fun setMediaSources(mediaSources: MutableList, resetPosition: Boolean) {} + override fun setMediaSources(mediaSources: MutableList, startMediaItemIndex: Int, startPositionMs: Long) {} + override fun setMediaSource(mediaSource: MediaSource) {} + override fun setMediaSource(mediaSource: MediaSource, startPositionMs: Long) {} + override fun setMediaSource(mediaSource: MediaSource, resetPosition: Boolean) {} + override fun addMediaSource(mediaSource: MediaSource) {} + override fun addMediaSource(index: Int, mediaSource: MediaSource) {} + override fun addMediaSources(mediaSources: MutableList) {} + override fun addMediaSources(index: Int, mediaSources: MutableList) {} + override fun setShuffleOrder(shuffleOrder: ShuffleOrder) {} + override fun setPreloadConfiguration(preloadConfiguration: ExoPlayer.PreloadConfiguration) {} + override fun getPreloadConfiguration(): ExoPlayer.PreloadConfiguration = throw NotImplementedError() + override fun setAudioSessionId(audioSessionId: Int) {} + override fun getAudioSessionId(): Int = throw NotImplementedError() + override fun setAuxEffectInfo(auxEffectInfo: AuxEffectInfo) {} + override fun clearAuxEffectInfo() {} + override fun setPreferredAudioDevice(audioDeviceInfo: AudioDeviceInfo?) {} + override fun setSkipSilenceEnabled(skipSilenceEnabled: Boolean) {} + override fun getSkipSilenceEnabled(): Boolean = throw NotImplementedError() + override fun setVideoEffects(videoEffects: MutableList) {} + override fun setVideoScalingMode(videoScalingMode: Int) {} + override fun getVideoScalingMode(): Int = throw NotImplementedError() + override fun setVideoChangeFrameRateStrategy(videoChangeFrameRateStrategy: Int) {} + override fun getVideoChangeFrameRateStrategy(): Int = throw NotImplementedError() + override fun setVideoFrameMetadataListener(listener: VideoFrameMetadataListener) {} + override fun clearVideoFrameMetadataListener(listener: VideoFrameMetadataListener) {} + override fun setCameraMotionListener(listener: CameraMotionListener) {} + override fun clearCameraMotionListener(listener: CameraMotionListener) {} + override fun createMessage(target: PlayerMessage.Target): PlayerMessage = throw NotImplementedError() + override fun setSeekParameters(seekParameters: SeekParameters?) {} + override fun getSeekParameters(): SeekParameters = throw NotImplementedError() + override fun setForegroundMode(foregroundMode: Boolean) {} + override fun setPauseAtEndOfMediaItems(pauseAtEndOfMediaItems: Boolean) {} + override fun getPauseAtEndOfMediaItems(): Boolean = throw NotImplementedError() + override fun getAudioFormat(): Format? = throw NotImplementedError() + override fun getVideoFormat(): Format? = throw NotImplementedError() + override fun getAudioDecoderCounters(): DecoderCounters? = throw NotImplementedError() + override fun getVideoDecoderCounters(): DecoderCounters? = throw NotImplementedError() + override fun setHandleAudioBecomingNoisy(handleAudioBecomingNoisy: Boolean) {} + override fun setWakeMode(wakeMode: Int) {} + override fun setPriority(priority: Int) {} + override fun setPriorityTaskManager(priorityTaskManager: PriorityTaskManager?) {} + override fun isSleepingForOffload(): Boolean = throw NotImplementedError() + override fun isTunnelingEnabled(): Boolean = throw NotImplementedError() + override fun isReleased(): Boolean = throw NotImplementedError() + override fun setImageOutput(imageOutput: ImageOutput?) {} +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerWrapper.kt similarity index 93% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerWrapper.kt index 740f1675e05..fef307c155a 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerWrapper.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.local.exoplayer +package io.element.android.libraries.mediaviewer.impl.local.video import android.content.Context import androidx.media3.common.Player diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt similarity index 83% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerState.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt index f5197af99be..c4e4b913a72 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.player +package io.element.android.libraries.mediaviewer.impl.local.video data class MediaPlayerControllerState( val isVisible: Boolean, diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt similarity index 94% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerStateProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt index 5cffb639904..78059bd4eb7 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.player +package io.element.android.libraries.mediaviewer.impl.local.video import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerView.kt similarity index 98% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerView.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerView.kt index 188d4e0c4a3..9cee07728f1 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/player/MediaPlayerControllerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerView.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.player +package io.element.android.libraries.mediaviewer.impl.local.video import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt new file mode 100644 index 00000000000..d9785491924 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.local.video + +import android.annotation.SuppressLint +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.KeepScreenOn +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState +import io.element.android.libraries.mediaviewer.impl.local.PlayableState +import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds + +@SuppressLint("UnsafeOptInUsageError") +@Composable +fun MediaVideoView( + localMediaViewState: LocalMediaViewState, + localMedia: LocalMedia?, + modifier: Modifier = Modifier, +) { + val exoPlayer = if (LocalInspectionMode.current) { + remember { + ExoPlayerForPreview() + } + } else { + val context = LocalContext.current + remember { + ExoPlayerWrapper.create(context) + } + } + ExoPlayerMediaVideoView( + localMediaViewState = localMediaViewState, + exoPlayer = exoPlayer, + localMedia = localMedia, + modifier = modifier, + ) +} + +@SuppressLint("UnsafeOptInUsageError") +@Composable +private fun ExoPlayerMediaVideoView( + localMediaViewState: LocalMediaViewState, + exoPlayer: ExoPlayer, + localMedia: LocalMedia?, + modifier: Modifier = Modifier, +) { + val isControllerVisibleByDefault = LocalInspectionMode.current + var mediaPlayerControllerState: MediaPlayerControllerState by remember { + mutableStateOf( + MediaPlayerControllerState( + isVisible = isControllerVisibleByDefault, + isPlaying = false, + progressInMillis = 0, + durationInMillis = 0, + isMuted = false, + ) + ) + } + + val playableState: PlayableState.Playable by remember { + derivedStateOf { + PlayableState.Playable( + isShowingControls = mediaPlayerControllerState.isVisible, + ) + } + } + + localMediaViewState.playableState = playableState + + val playerListener = object : Player.Listener { + override fun onRenderedFirstFrame() { + localMediaViewState.isReady = true + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isPlaying = isPlaying, + ) + } + + override fun onVolumeChanged(volume: Float) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isMuted = volume == 0f, + ) + } + + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + durationInMillis = exoPlayer.duration, + ) + } + } + } + + LaunchedEffect(Unit) { + exoPlayer.addListener(playerListener) + exoPlayer.prepare() + } + + var autoHideController by remember { mutableIntStateOf(0) } + + LaunchedEffect(autoHideController) { + delay(5.seconds) + if (exoPlayer.isPlaying) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isVisible = false, + ) + } + } + + LaunchedEffect(exoPlayer.isPlaying) { + if (exoPlayer.isPlaying) { + while (true) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + progressInMillis = exoPlayer.currentPosition, + ) + delay(200) + } + } else { + // Ensure we render the final state + mediaPlayerControllerState = mediaPlayerControllerState.copy( + progressInMillis = exoPlayer.currentPosition, + ) + } + } + if (localMedia?.uri != null) { + LaunchedEffect(localMedia.uri) { + val mediaItem = MediaItem.fromUri(localMedia.uri) + exoPlayer.setMediaItem(mediaItem) + } + } else { + exoPlayer.setMediaItems(emptyList()) + } + KeepScreenOn(mediaPlayerControllerState.isPlaying) + Box( + modifier = modifier + .background(ElementTheme.colors.bgSubtlePrimary), + ) { + val context = LocalContext.current + if (LocalInspectionMode.current) { + Text( + modifier = Modifier + .background(ElementTheme.colors.bgSubtlePrimary) + .align(Alignment.Center), + text = "A Video Player will render here", + ) + } else { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { + PlayerView(context).apply { + player = exoPlayer + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + setOnClickListener { + autoHideController++ + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isVisible = !mediaPlayerControllerState.isVisible, + ) + } + useController = false + } + }, + onRelease = { playerView -> + playerView.setOnClickListener(null) + playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?) + playerView.player = null + }, + ) + } + MediaPlayerControllerView( + state = mediaPlayerControllerState, + onTogglePlay = { + autoHideController++ + if (exoPlayer.isPlaying) { + exoPlayer.pause() + } else { + if (exoPlayer.playbackState == Player.STATE_ENDED) { + exoPlayer.seekTo(0) + } else { + exoPlayer.play() + } + } + }, + onSeekChange = { + autoHideController++ + if (exoPlayer.isPlaying.not()) { + exoPlayer.play() + } + exoPlayer.seekTo(it.toLong()) + }, + onToggleMute = { + autoHideController++ + exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f + }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + } + + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> exoPlayer.play() + Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() + Lifecycle.Event.ON_DESTROY -> { + exoPlayer.release() + exoPlayer.removeListener(playerListener) + } + else -> Unit + } + } +} + +@PreviewsDayNight +@Composable +internal fun MediaVideoViewPreview() = ElementPreview { + MediaVideoView( + modifier = Modifier.fillMaxSize(), + localMediaViewState = rememberLocalMediaViewState(), + localMedia = null, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt new file mode 100644 index 00000000000..026be26b2ed --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidation.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.util + +import android.webkit.MimeTypeMap +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor { + override fun extractFromName(name: String): String { + val fileExtension = name.substringAfterLast('.', "") + // Makes sure the extension is known by the system, otherwise default to binary extension. + return if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) { + fileExtension + } else { + "bin" + } + } +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt similarity index 87% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt index f72becb58ff..ac2714584c1 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer sealed interface MediaViewerEvents { data object SaveOnDisk : MediaViewerEvents diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt similarity index 69% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 7d173f52913..83c7c1aca76 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -5,22 +5,21 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme -import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint @ContributesNode(RoomScope::class) open class MediaViewerNode @AssistedInject constructor( @@ -28,15 +27,13 @@ open class MediaViewerNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: MediaViewerPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - data class Inputs( - val mediaInfo: MediaInfo, - val mediaSource: MediaSource, - val thumbnailSource: MediaSource?, - val canDownload: Boolean, - val canShare: Boolean, - ) : NodeInputs + private val inputs = inputs() - private val inputs: Inputs = inputs() + private fun onDone() { + plugins().forEach { + it.onDone() + } + } private val presenter = presenterFactory.create(inputs) @@ -47,7 +44,7 @@ open class MediaViewerNode @AssistedInject constructor( MediaViewerView( state = state, modifier = modifier, - onBackClick = this::navigateUp + onBackClick = ::onDone ) } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt similarity index 94% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index 3cef900040b..068fb02b0f7 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer import android.content.ActivityNotFoundException import androidx.compose.runtime.Composable @@ -27,16 +27,17 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR class MediaViewerPresenter @AssistedInject constructor( - @Assisted private val inputs: MediaViewerNode.Inputs, + @Assisted private val inputs: MediaViewerEntryPoint.Params, private val localMediaFactory: LocalMediaFactory, private val mediaLoader: MatrixMediaLoader, private val localMediaActions: LocalMediaActions, @@ -44,7 +45,7 @@ class MediaViewerPresenter @AssistedInject constructor( ) : Presenter { @AssistedFactory interface Factory { - fun create(inputs: MediaViewerNode.Inputs): MediaViewerPresenter + fun create(inputs: MediaViewerEntryPoint.Params): MediaViewerPresenter } @Composable diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt similarity index 85% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt index 3dd9f1b3722..94d6653241c 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt @@ -5,13 +5,13 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.MediaInfo data class MediaViewerState( val mediaInfo: MediaInfo, diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt similarity index 84% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 2ae215ac967..4ffcb75b075 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -5,18 +5,18 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo +import io.element.android.libraries.mediaviewer.api.anApkMediaInfo +import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.aPdfMediaInfo -import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt similarity index 95% rename from libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 9f362c13fa3..ae5e9401cb9 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -7,8 +7,9 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn @@ -55,12 +56,12 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData -import io.element.android.libraries.mediaviewer.api.R +import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.LocalMediaView -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.PlayableState -import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState +import io.element.android.libraries.mediaviewer.impl.R +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaView +import io.element.android.libraries.mediaviewer.impl.local.PlayableState +import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.delay import me.saket.telephoto.flick.FlickToDismiss @@ -79,6 +80,7 @@ fun MediaViewerView( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) var showOverlay by remember { mutableStateOf(true) } + BackHandler { onBackClick() } Scaffold( modifier, containerColor = Color.Transparent, @@ -146,8 +148,8 @@ private fun MediaViewerPage( Box( modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + .fillMaxSize() + .navigationBarsPadding() ) { Box(contentAlignment = Alignment.Center) { val zoomableState = rememberZoomableState( @@ -191,8 +193,8 @@ private fun MediaViewerPage( if (showProgress) { LinearProgressIndicator( modifier = Modifier - .fillMaxWidth() - .height(2.dp) + .fillMaxWidth() + .height(2.dp) ) } } diff --git a/libraries/mediaviewer/api/src/main/res/drawable/ic_apk_install.xml b/libraries/mediaviewer/impl/src/main/res/drawable/ic_apk_install.xml similarity index 100% rename from libraries/mediaviewer/api/src/main/res/drawable/ic_apk_install.xml rename to libraries/mediaviewer/impl/src/main/res/drawable/ic_apk_install.xml diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt index eb78517a36a..5729c5310da 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt @@ -12,9 +12,9 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.test.media.FakeMediaFile -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo -import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo +import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt similarity index 70% rename from libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt index a9ab18ce616..df16eb62e87 100644 --- a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.util +package io.element.android.libraries.mediaviewer.impl.util import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -13,7 +13,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class FileExtensionExtractorTest { +class FileExtensionExtractorWithValidationTest { @Test fun `test FileExtensionExtractor with validation OK`() { val sut = FileExtensionExtractorWithValidation() @@ -27,11 +27,4 @@ class FileExtensionExtractorTest { val sut = FileExtensionExtractorWithValidation() assertThat(sut.extractFromName("test.bla")).isEqualTo("bin") } - - @Test - fun `test FileExtensionExtractor no validation`() { - val sut = FileExtensionExtractorWithoutValidation() - assertThat(sut.extractFromName("test.png")).isEqualTo("png") - assertThat(sut.extractFromName("test.bla")).isEqualTo("bla") - } } diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt similarity index 94% rename from libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index a54ccf2cc2f..cbe334216cd 100644 --- a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -7,7 +7,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.libraries.mediaviewer +package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import app.cash.molecule.RecompositionMode @@ -18,10 +18,8 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource -import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode -import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.tests.testutils.WarmUpRule @@ -144,7 +142,7 @@ class MediaViewerPresenterTest { canDownload: Boolean = true, ): MediaViewerPresenter { return MediaViewerPresenter( - inputs = MediaViewerNode.Inputs( + inputs = MediaViewerEntryPoint.Params( mediaInfo = TESTED_MEDIA_INFO, mediaSource = aMediaSource(), thumbnailSource = null, diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt similarity index 97% rename from libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index ee5aef8e2ef..acbfb576190 100644 --- a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.api.viewer +package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.activity.ComponentActivity @@ -18,8 +18,8 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts index aa182faa987..47adf35975c 100644 --- a/libraries/mediaviewer/test/build.gradle.kts +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -18,4 +18,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.tests.testutils) implementation(projects.libraries.matrix.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) } diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt index b6437c212b4..c8555e5bce3 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.mediaviewer.test import androidx.compose.runtime.Composable import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions +import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions import io.element.android.tests.testutils.simulateLongTask class FakeLocalMediaActions : LocalMediaActions { diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt index 43474e24bb5..df85f2d53d7 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt @@ -10,11 +10,11 @@ package io.element.android.libraries.mediaviewer.test import android.net.Uri import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory -import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor -import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia class FakeLocalMediaFactory( diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidation.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidation.kt new file mode 100644 index 00000000000..c252c216aac --- /dev/null +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidation.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.test.util + +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor + +class FileExtensionExtractorWithoutValidation : FileExtensionExtractor { + override fun extractFromName(name: String): String { + return name.substringAfterLast('.', "") + } +} diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt index ea2953f69c4..4f933b1c008 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt @@ -8,9 +8,9 @@ package io.element.android.libraries.mediaviewer.test.viewer import android.net.Uri +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo fun aLocalMedia( uri: Uri, diff --git a/libraries/mediaviewer/test/src/test/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidationTest.kt b/libraries/mediaviewer/test/src/test/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidationTest.kt new file mode 100644 index 00000000000..48a6fbac117 --- /dev/null +++ b/libraries/mediaviewer/test/src/test/kotlin/io/element/android/libraries/mediaviewer/test/util/FileExtensionExtractorWithoutValidationTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.test.util + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class FileExtensionExtractorWithoutValidationTest { + @Test + fun `extension should always be extracted even is invalid`() { + val sut = FileExtensionExtractorWithoutValidation() + assertThat(sut.extractFromName("test.png")).isEqualTo("png") + assertThat(sut.extractFromName("test.bla")).isEqualTo("bla") + } +} diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 6fbb18e1874..b690dd492ce 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -169,26 +169,20 @@ fun Project.setupKover() { filters { excludes.classes( "*State$*", // Exclude inner classes - "io.element.android.appnav.root.RootNavState*", - "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", - "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*", - "io.element.android.libraries.matrix.api.room.RoomMembershipState*", - "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*", - "io.element.android.libraries.push.impl.notifications.NotificationState*", - "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState", - "io.element.android.features.messages.impl.media.local.LocalMediaViewState", - "io.element.android.features.location.impl.map.MapState*", - "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", - "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*", - "io.element.android.features.messages.impl.timeline.components.ExpandableState*", - "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*", - "io.element.android.libraries.maplibre.compose.CameraPositionState*", - "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", - "io.element.android.libraries.maplibre.compose.SymbolState*", + "io.element.android.appnav.root.RootNavState", "io.element.android.features.ftue.api.state.*", "io.element.android.features.ftue.impl.welcome.state.*", + "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState", + "io.element.android.libraries.designsystem.swipe.SwipeableActionsState", "io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState", - "io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewerState", + "io.element.android.libraries.maplibre.compose.CameraPositionState", + "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", + "io.element.android.libraries.maplibre.compose.SymbolState", + "io.element.android.libraries.matrix.api.room.RoomMembershipState", + "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState", + "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", + "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", + "io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState", "io.element.android.libraries.textcomposer.model.TextEditorState", ) includes.classes("*State") diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png index 3982a58c4ca..8510d977be9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d18f7e2c169f7e4b9e435584d868454ae5f765a32d29bbbfe48d65b45f4161d6 -size 18845 +oid sha256:3a92cb782d97553f364fab3df3fe159557a26aad8b7e5c176b40616c2f05abdd +size 51410 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png index 2cb5dc89f83..e799726c52b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3250fa9767aec264308719c6fdf6cea94d5ccea21bfa8a735d7df5d79b572555 -size 21214 +oid sha256:361ff23fd2ff99aecfdffd10b45e9235d86183d4856cb2a3e99f85b9e04c2d59 +size 51376 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png index e34eb6e69c0..df965394aa5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ba70221a149ce0f8abaa4e4f870011bea82630edf99ecacea560a737f56346f -size 22215 +oid sha256:efbfed755b29293009f45fca33f58863b612772de9a1d55593c979dbb04ff6f2 +size 88981 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png index 8510d977be9..6920a089ef4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a92cb782d97553f364fab3df3fe159557a26aad8b7e5c176b40616c2f05abdd -size 51410 +oid sha256:507c7dffae1aabfa687174f1f964e2c40b004183b6bc3a70b56d764e0d308b47 +size 392923 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png deleted file mode 100644 index e799726c52b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:361ff23fd2ff99aecfdffd10b45e9235d86183d4856cb2a3e99f85b9e04c2d59 -size 51376 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png deleted file mode 100644 index df965394aa5..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efbfed755b29293009f45fca33f58863b612772de9a1d55593c979dbb04ff6f2 -size 88981 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png deleted file mode 100644 index 6920a089ef4..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:507c7dffae1aabfa687174f1f964e2c40b004183b6bc3a70b56d764e0d308b47 -size 392923 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_4_en.png deleted file mode 100644 index 67b4bf5eae1..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be67078d37af27ed532a6ae3a6194c154ca72b7c87319eed3f0e982cbc677acc -size 10908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png new file mode 100644 index 00000000000..50f49e2d0bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2907ba5f75a40e30a37ad4d302797cea4e3d2b77cb1c66a9432319242b8f1995 +size 12188 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png new file mode 100644 index 00000000000..c4d024965ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5307e90428957819812269b9b3e0c6e9d59238141d54cd959aa5506290797a35 +size 11587 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png new file mode 100644 index 00000000000..37bc2056260 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59e981e51595a368745c92f355793b2c0301a9e4929430733876a02f3ac75e2e +size 11881 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png new file mode 100644 index 00000000000..37f1c2ed228 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52354bcf471b14e38e582cc29f73407c8ca65026b2b7c6db3d3b28ec94950679 +size 11413 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en.png new file mode 100644 index 00000000000..0d80d15a73f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a3e650bde2df79bebfc110eb46998ab21b1e5dba471fd5a755371e5027a27a5 +size 388634 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en.png new file mode 100644 index 00000000000..c8e897025bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:193190902275db6ffd03dcc41b190e8fc3bba60dda9678274481fbbefe89567d +size 387830 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.impl.local.pdf_PdfPagesErrorView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en.png 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.impl.local.pdf_PdfPagesErrorView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png new file mode 100644 index 00000000000..ec685d363a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b7160dc01dd4704bb07a8c91858bef0d4e99c0f55ca1d7dbdc3e22546a25125 +size 12210 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png new file mode 100644 index 00000000000..df1ea796c15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2591b742c64d3d058d4638e5014080f8b3eb13eba3a73951d71dfdc60e7676d9 +size 12455 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_10_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_1_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_2_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_3_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png new file mode 100644 index 00000000000..d5d6d31f124 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05794effa4c47e3d2b68892e613f18a55c96789a4e65a182dd0bf2ca0c812d44 +size 14474 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_5_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_6_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_7_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_8_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.api.viewer_MediaViewerView_9_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png