Media: rework how we get the activity context (and fix test compilation)
This commit is contained in:
parent
61fc57d3ef
commit
4a71aab95e
6 changed files with 90 additions and 24 deletions
|
|
@ -25,6 +25,9 @@ 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.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
|
@ -46,6 +49,19 @@ class AndroidLocalMediaActions @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : LocalMediaActions {
|
) : LocalMediaActions {
|
||||||
|
|
||||||
|
private var activityContext: Context? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Configure() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return DisposableEffect(Unit) {
|
||||||
|
activityContext = context
|
||||||
|
onDispose {
|
||||||
|
activityContext = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
@ -61,7 +77,7 @@ class AndroidLocalMediaActions @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun share(activityContext: Context, localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
override suspend fun share(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
||||||
runCatching {
|
runCatching {
|
||||||
val shareableUri = localMedia.toShareableUri()
|
val shareableUri = localMedia.toShareableUri()
|
||||||
|
|
@ -71,7 +87,7 @@ class AndroidLocalMediaActions @Inject constructor(
|
||||||
.setTypeAndNormalize(localMedia.info.mimeType)
|
.setTypeAndNormalize(localMedia.info.mimeType)
|
||||||
withContext(coroutineDispatchers.main) {
|
withContext(coroutineDispatchers.main) {
|
||||||
val intent = Intent.createChooser(shareMediaIntent, null)
|
val intent = Intent.createChooser(shareMediaIntent, null)
|
||||||
activityContext.startActivity(intent)
|
activityContext!!.startActivity(intent)
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Timber.v("Share media succeed")
|
Timber.v("Share media succeed")
|
||||||
|
|
@ -80,14 +96,14 @@ class AndroidLocalMediaActions @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun open(activityContext: Context, localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
override suspend fun open(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
||||||
runCatching {
|
runCatching {
|
||||||
val openMediaIntent = Intent(Intent.ACTION_VIEW)
|
val openMediaIntent = Intent(Intent.ACTION_VIEW)
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
|
.setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType)
|
||||||
withContext(coroutineDispatchers.main) {
|
withContext(coroutineDispatchers.main) {
|
||||||
activityContext.startActivity(openMediaIntent)
|
activityContext!!.startActivity(openMediaIntent)
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Timber.v("Open media succeed")
|
Timber.v("Open media succeed")
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.media.local
|
package io.element.android.features.messages.impl.media.local
|
||||||
|
|
||||||
import android.content.Context
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
interface LocalMediaActions {
|
interface LocalMediaActions {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Configure()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will save the current media to the Downloads directory.
|
* Will save the current media to the Downloads directory.
|
||||||
* The [LocalMedia.uri] needs to have a file scheme.
|
* The [LocalMedia.uri] needs to have a file scheme.
|
||||||
|
|
@ -29,12 +33,12 @@ interface LocalMediaActions {
|
||||||
* Will try to find a suitable application to share the media with.
|
* Will try to find a suitable application to share the media with.
|
||||||
* The [LocalMedia.uri] needs to have a file scheme.
|
* The [LocalMedia.uri] needs to have a file scheme.
|
||||||
*/
|
*/
|
||||||
suspend fun share(activityContext: Context, localMedia: LocalMedia): Result<Unit>
|
suspend fun share(localMedia: LocalMedia): Result<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will try to find a suitable application to open the media with.
|
* Will try to find a suitable application to open the media with.
|
||||||
* The [LocalMedia.uri] needs to have a file scheme.
|
* The [LocalMedia.uri] needs to have a file scheme.
|
||||||
*/
|
*/
|
||||||
suspend fun open(activityContext: Context, localMedia: LocalMedia): Result<Unit>
|
suspend fun open(localMedia: LocalMedia): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.media.viewer
|
package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
|
@ -26,7 +25,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -50,7 +48,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: LocalMediaActions,
|
private val localMediaActions: LocalMediaActions,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<MediaViewerState> {
|
) : Presenter<MediaViewerState> {
|
||||||
|
|
||||||
|
|
@ -69,8 +67,8 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
val localMedia: MutableState<Async<LocalMedia>> = remember {
|
val localMedia: MutableState<Async<LocalMedia>> = remember {
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
val context = LocalContext.current
|
|
||||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||||
|
localMediaActions.Configure()
|
||||||
DisposableEffect(loadMediaTrigger) {
|
DisposableEffect(loadMediaTrigger) {
|
||||||
coroutineScope.downloadMedia(mediaFile, localMedia)
|
coroutineScope.downloadMedia(mediaFile, localMedia)
|
||||||
onDispose {
|
onDispose {
|
||||||
|
|
@ -83,8 +81,8 @@ 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(context, localMedia.value)
|
MediaViewerEvents.Share -> coroutineScope.share(localMedia.value)
|
||||||
MediaViewerEvents.OpenWith -> coroutineScope.open(context, localMedia.value)
|
MediaViewerEvents.OpenWith -> coroutineScope.open(localMedia.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +119,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
|
||||||
when (localMedia) {
|
when (localMedia) {
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
mediaActionsHandler.saveOnDisk(localMedia.state)
|
localMediaActions.saveOnDisk(localMedia.state)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android)
|
val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android)
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
|
|
@ -131,10 +129,10 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.share(activityContext: Context, localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
|
||||||
when (localMedia) {
|
when (localMedia) {
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
mediaActionsHandler.share(activityContext, localMedia.state)
|
localMediaActions.share(localMedia.state)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val snackbarMessage = SnackbarMessage(openShareError(it))
|
val snackbarMessage = SnackbarMessage(openShareError(it))
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
|
|
@ -144,10 +142,10 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.open(activityContext: Context, localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
|
||||||
when (localMedia) {
|
when (localMedia) {
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
mediaActionsHandler.open(activityContext, localMedia.state)
|
localMediaActions.open(localMedia.state)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val snackbarMessage = SnackbarMessage(openShareError(it))
|
val snackbarMessage = SnackbarMessage(openShareError(it))
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.media
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
|
import io.element.android.features.messages.impl.media.local.LocalMediaActions
|
||||||
|
|
||||||
|
class FakeLocalMediaActions: LocalMediaActions {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Configure() {
|
||||||
|
//NOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> {
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun share(localMedia: LocalMedia): Result<Unit> {
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun open(localMedia: LocalMedia): Result<Unit> {
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ class FakeLocalMediaFactory(private val localMediaUri: Uri) : LocalMediaFactory
|
||||||
|
|
||||||
var fallbackMimeType: String = MimeTypes.OctetStream
|
var fallbackMimeType: String = MimeTypes.OctetStream
|
||||||
|
|
||||||
override fun createFromUri(uri: Uri, mimeType: String?, name: String?): LocalMedia {
|
override fun createFromUri(uri: Uri, mimeType: String?, name: String?, formattedFileSize: String?): LocalMedia {
|
||||||
return aLocalMedia(uri, mimeType ?: fallbackMimeType)
|
return aLocalMedia(uri, mimeType ?: fallbackMimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,14 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||||
import io.element.android.features.messages.impl.media.viewer.MediaViewerEvents
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerEvents
|
||||||
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
|
||||||
import io.element.android.features.messages.impl.media.viewer.MediaViewerPresenter
|
import io.element.android.features.messages.impl.media.viewer.MediaViewerPresenter
|
||||||
|
import io.element.android.features.messages.media.FakeLocalMediaActions
|
||||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
||||||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
|
|
@ -54,7 +57,7 @@ class MediaViewerPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
||||||
assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME)
|
assertThat(initialState.mediaInfo.name).isEqualTo(TESTED_MEDIA_NAME)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
||||||
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS + 1)
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS + 1)
|
||||||
|
|
@ -74,7 +77,7 @@ class MediaViewerPresenterTest {
|
||||||
mediaLoader.shouldFail = true
|
mediaLoader.shouldFail = true
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized)
|
||||||
assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME)
|
assertThat(initialState.mediaInfo.name).isEqualTo(TESTED_MEDIA_NAME)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java)
|
||||||
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS)
|
||||||
|
|
@ -97,13 +100,17 @@ class MediaViewerPresenterTest {
|
||||||
private fun aMediaViewerPresenter(mimeType: String = TESTED_MIME_TYPE): MediaViewerPresenter {
|
private fun aMediaViewerPresenter(mimeType: String = TESTED_MIME_TYPE): MediaViewerPresenter {
|
||||||
return MediaViewerPresenter(
|
return MediaViewerPresenter(
|
||||||
inputs = MediaViewerNode.Inputs(
|
inputs = MediaViewerNode.Inputs(
|
||||||
name = TESTED_MEDIA_NAME,
|
mediaInfo = MediaInfo(name = TESTED_MEDIA_NAME,
|
||||||
|
mimeType = mimeType,
|
||||||
|
formattedFileSize = "14MB"
|
||||||
|
),
|
||||||
mediaSource = aMediaSource(),
|
mediaSource = aMediaSource(),
|
||||||
mimeType = mimeType,
|
|
||||||
thumbnailSource = null
|
thumbnailSource = null
|
||||||
),
|
),
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
mediaLoader = mediaLoader
|
mediaLoader = mediaLoader,
|
||||||
|
localMediaActions = FakeLocalMediaActions(),
|
||||||
|
snackbarDispatcher = SnackbarDispatcher()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue