diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActionsHandler.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt similarity index 70% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActionsHandler.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt index 1e3056e146..eb1773042b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActionsHandler.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt @@ -18,25 +18,31 @@ package io.element.android.features.messages.impl.media.local import android.content.ContentValues import android.content.Context +import android.content.Intent import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.annotation.RequiresApi +import androidx.core.content.FileProvider +import androidx.core.net.toFile import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.io.InputStream import javax.inject.Inject @ContributesBinding(AppScope::class) -class AndroidLocalMediaActionsHandler @Inject constructor( +class AndroidLocalMediaActions @Inject constructor( @ApplicationContext private val context: Context, private val coroutineDispatchers: CoroutineDispatchers, -) : LocalMediaActionsHandler { + private val buildMeta: BuildMeta, +) : LocalMediaActions { override suspend fun saveOnDisk(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { runCatching { @@ -45,11 +51,28 @@ class AndroidLocalMediaActionsHandler @Inject constructor( } else { saveOnDiskUsingExternalStorageApi(localMedia) } + }.onSuccess { + Timber.v("Save on disk succeed") + }.onFailure { + Timber.e(it, "Save on disk failed") } } - override suspend fun share(localMedia: LocalMedia): Result { - TODO("Not yet implemented") + override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + runCatching { + val authority = "${buildMeta.applicationId}.fileprovider" + val uriFromFileProvider = FileProvider.getUriForFile(context, authority, localMedia.toFile()) + val shareMediaIntent = Intent(Intent.ACTION_VIEW) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + .setDataAndType(uriFromFileProvider, localMedia.mimeType) + withContext(coroutineDispatchers.main) { + context.startActivity(shareMediaIntent, null) + } + }.onSuccess { + Timber.v("Share media succeed") + }.onFailure { + Timber.e(it, "Share media failed") + } } @RequiresApi(Build.VERSION_CODES.Q) @@ -85,4 +108,8 @@ class AndroidLocalMediaActionsHandler @Inject constructor( private fun LocalMedia.openStream(): InputStream? { return context.contentResolver.openInputStream(uri) } + + private fun LocalMedia.toFile(): File { + return uri.toFile() + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActionsHandler.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActionsHandler.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt index 7af7110b70..f7c37bd14a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActionsHandler.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt @@ -16,7 +16,7 @@ package io.element.android.features.messages.impl.media.local -interface LocalMediaActionsHandler { +interface LocalMediaActions { suspend fun saveOnDisk(localMedia: LocalMedia): Result suspend fun share(localMedia: LocalMedia): Result } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt index 030ee6b269..375b7e4a34 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.media.viewer sealed interface MediaViewerEvents { object SaveOnDisk: MediaViewerEvents + object Share: MediaViewerEvents object RetryLoading : MediaViewerEvents object ClearLoadingError : MediaViewerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 54e2175257..b8e175424a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -28,7 +28,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.LocalMediaActionsHandler +import io.element.android.features.messages.impl.media.local.LocalMediaActions import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.features.messages.impl.media.local.createFromMediaFile import io.element.android.libraries.architecture.Async @@ -42,7 +42,7 @@ class MediaViewerPresenter @AssistedInject constructor( @Assisted private val inputs: MediaViewerNode.Inputs, private val localMediaFactory: LocalMediaFactory, private val mediaLoader: MatrixMediaLoader, - private val mediaActionsHandler: LocalMediaActionsHandler, + private val mediaActionsHandler: LocalMediaActions, ) : Presenter { @AssistedFactory @@ -72,6 +72,7 @@ class MediaViewerPresenter @AssistedInject constructor( MediaViewerEvents.RetryLoading -> loadMediaTrigger++ MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value) + MediaViewerEvents.Share -> coroutineScope.share(localMedia.value) } } @@ -102,11 +103,20 @@ class MediaViewerPresenter @AssistedInject constructor( } } - private fun CoroutineScope.saveOnDisk(value: Async) = launch { - when (value) { - is Async.Success -> mediaActionsHandler.saveOnDisk(value.state) + private fun CoroutineScope.saveOnDisk(localMedia: Async) = launch { + when (localMedia) { + is Async.Success -> mediaActionsHandler.saveOnDisk(localMedia.state) + else -> Unit + } + } + + private fun CoroutineScope.share(localMedia: Async) = launch { + when (localMedia) { + is Async.Success -> mediaActionsHandler.share(localMedia.state) else -> Unit } } } + + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index f821f488c9..721cb76350 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -137,6 +137,13 @@ private fun MediaViewerTopBar( title = {}, navigationIcon = { BackButton(onClick = onBackPressed) }, actions = { + IconButton( + onClick = { + eventSink(MediaViewerEvents.Share) + }, + ) { + Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = string.action_share)) + } IconButton( onClick = { eventSink(MediaViewerEvents.SaveOnDisk) diff --git a/gradle.properties b/gradle.properties index ae25b1ed02..9b4f41f685 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -org.gradle.caching=true +org.gradle.caching=false org.gradle.configureondemand=true org.gradle.parallel=true