Media: implements share action

This commit is contained in:
ganfra 2023-06-02 16:43:28 +02:00
parent 7386936217
commit 1c01c0a6cc
6 changed files with 57 additions and 12 deletions

View file

@ -18,25 +18,31 @@ package io.element.android.features.messages.impl.media.local
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class AndroidLocalMediaActionsHandler @Inject constructor( class AndroidLocalMediaActions @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : LocalMediaActionsHandler { private val buildMeta: BuildMeta,
) : LocalMediaActions {
override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching { runCatching {
@ -45,11 +51,28 @@ class AndroidLocalMediaActionsHandler @Inject constructor(
} else { } else {
saveOnDiskUsingExternalStorageApi(localMedia) saveOnDiskUsingExternalStorageApi(localMedia)
} }
}.onSuccess {
Timber.v("Save on disk succeed")
}.onFailure {
Timber.e(it, "Save on disk failed")
} }
} }
override suspend fun share(localMedia: LocalMedia): Result<Unit> { override suspend fun share(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
TODO("Not yet implemented") 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) @RequiresApi(Build.VERSION_CODES.Q)
@ -85,4 +108,8 @@ class AndroidLocalMediaActionsHandler @Inject constructor(
private fun LocalMedia.openStream(): InputStream? { private fun LocalMedia.openStream(): InputStream? {
return context.contentResolver.openInputStream(uri) return context.contentResolver.openInputStream(uri)
} }
private fun LocalMedia.toFile(): File {
return uri.toFile()
}
} }

View file

@ -16,7 +16,7 @@
package io.element.android.features.messages.impl.media.local package io.element.android.features.messages.impl.media.local
interface LocalMediaActionsHandler { interface LocalMediaActions {
suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit>
suspend fun share(localMedia: LocalMedia): Result<Unit> suspend fun share(localMedia: LocalMedia): Result<Unit>
} }

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.media.viewer
sealed interface MediaViewerEvents { sealed interface MediaViewerEvents {
object SaveOnDisk: MediaViewerEvents object SaveOnDisk: MediaViewerEvents
object Share: MediaViewerEvents
object RetryLoading : MediaViewerEvents object RetryLoading : MediaViewerEvents
object ClearLoadingError : MediaViewerEvents object ClearLoadingError : MediaViewerEvents
} }

View file

@ -28,7 +28,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.media.local.LocalMedia 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.LocalMediaFactory
import io.element.android.features.messages.impl.media.local.createFromMediaFile import io.element.android.features.messages.impl.media.local.createFromMediaFile
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
@ -42,7 +42,7 @@ class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerNode.Inputs, @Assisted private val inputs: MediaViewerNode.Inputs,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val mediaActionsHandler: LocalMediaActionsHandler, private val mediaActionsHandler: LocalMediaActions,
) : Presenter<MediaViewerState> { ) : Presenter<MediaViewerState> {
@AssistedFactory @AssistedFactory
@ -72,6 +72,7 @@ class MediaViewerPresenter @AssistedInject constructor(
MediaViewerEvents.RetryLoading -> loadMediaTrigger++ MediaViewerEvents.RetryLoading -> loadMediaTrigger++
MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized
MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value) 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<LocalMedia>) = launch { private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
when (value) { when (localMedia) {
is Async.Success -> mediaActionsHandler.saveOnDisk(value.state) is Async.Success -> mediaActionsHandler.saveOnDisk(localMedia.state)
else -> Unit
}
}
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
when (localMedia) {
is Async.Success -> mediaActionsHandler.share(localMedia.state)
else -> Unit else -> Unit
} }
} }
} }

View file

@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download 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.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -137,6 +137,13 @@ private fun MediaViewerTopBar(
title = {}, title = {},
navigationIcon = { BackButton(onClick = onBackPressed) }, navigationIcon = { BackButton(onClick = onBackPressed) },
actions = { actions = {
IconButton(
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = string.action_share))
}
IconButton( IconButton(
onClick = { onClick = {
eventSink(MediaViewerEvents.SaveOnDisk) eventSink(MediaViewerEvents.SaveOnDisk)

View file

@ -35,7 +35,7 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.caching=true org.gradle.caching=false
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.parallel=true org.gradle.parallel=true