From c3cb68ea7571a1a3bbb3be3f862d02cf02b2a657 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Fri, 28 Oct 2022 13:57:40 +0530 Subject: [PATCH 01/13] Refactor: fix package --- .../au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt | 4 ++-- .../pocketcasts/endofyear/views/stories/StoryFake1View.kt | 2 +- .../pocketcasts/endofyear/views/stories/StoryFake2View.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index b3b23cf6b1c..6301e09fa51 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -43,8 +43,8 @@ import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1 import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake2 -import au.com.shiftyjelly.pocketcasts.endofyear.views.StoryFake1View -import au.com.shiftyjelly.pocketcasts.endofyear.views.StoryFake2View +import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake1View +import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake2View import au.com.shiftyjelly.pocketcasts.models.entity.Podcast import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.localization.R as LR diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt index 029936b7d05..e1a02beb083 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt @@ -1,4 +1,4 @@ -package au.com.shiftyjelly.pocketcasts.endofyear.views +package au.com.shiftyjelly.pocketcasts.endofyear.views.stories import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake2View.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake2View.kt index 75f59134c82..e2b67653f93 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake2View.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake2View.kt @@ -1,4 +1,4 @@ -package au.com.shiftyjelly.pocketcasts.endofyear.views +package au.com.shiftyjelly.pocketcasts.endofyear.views.stories import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth From a6779bd4b86172a6472b8452ea73529d82167650 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Fri, 28 Oct 2022 14:01:59 +0530 Subject: [PATCH 02/13] Add file utility to save bitmap to file --- .../pocketcasts/utils/FileUtilWrapper.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt index a06c13048a0..7ff547644dd 100644 --- a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt +++ b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt @@ -1,9 +1,38 @@ package au.com.shiftyjelly.pocketcasts.utils +import android.content.Context +import android.graphics.Bitmap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import javax.inject.Inject class FileUtilWrapper @Inject constructor() { fun deleteDirectoryContents(path: String) { FileUtil.deleteDirectoryContents(path) } + + suspend fun saveBitmapToFile( + image: Bitmap, + context: Context, + saveFolderName: String, + saveFileName: String, + ): File? = withContext(Dispatchers.IO) { + val imagesFolder = File(context.cacheDir, saveFolderName) + var file: File? = null + try { + imagesFolder.mkdirs() + file = File(imagesFolder, saveFileName) + val stream = FileOutputStream(file) + image.compress(Bitmap.CompressFormat.PNG, 90, stream) + stream.flush() + stream.close() + } catch (e: IOException) { + Timber.e("Error while saving image to file " + e.message) + } + file + } } From fd426897ee581bab3b2e4efafb12053c6bf476d2 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Fri, 28 Oct 2022 17:40:45 +0530 Subject: [PATCH 03/13] Save story and show share view --- .../pocketcasts/endofyear/StoriesFragment.kt | 30 ++++++++++ .../pocketcasts/endofyear/StoriesScreen.kt | 60 ++++++++++++++----- .../pocketcasts/endofyear/StoriesViewModel.kt | 22 +++++++ .../pocketcasts/endofyear/views/SnapShot.kt | 35 +++++++++++ .../src/main/res/values/strings.xml | 1 + .../pocketcasts/utils/FileUtilWrapper.kt | 4 +- 6 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt index 85f3ecd87ea..db84bbbd8c1 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt @@ -1,5 +1,6 @@ package au.com.shiftyjelly.pocketcasts.endofyear +import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater @@ -11,7 +12,11 @@ import androidx.fragment.app.viewModels import au.com.shiftyjelly.pocketcasts.compose.AppTheme import au.com.shiftyjelly.pocketcasts.ui.R import au.com.shiftyjelly.pocketcasts.ui.helper.StatusBarColor +import au.com.shiftyjelly.pocketcasts.utils.FileUtil import au.com.shiftyjelly.pocketcasts.views.fragments.BaseDialogFragment +import timber.log.Timber +import java.io.File +import au.com.shiftyjelly.pocketcasts.localization.R as LR class StoriesFragment : BaseDialogFragment() { private val viewModel: StoriesViewModel by viewModels() @@ -35,12 +40,37 @@ class StoriesFragment : BaseDialogFragment() { StoriesScreen( viewModel = viewModel, onCloseClicked = { dismiss() }, + onShareClicked = { onCaptureBitmap -> + viewModel.onShareClicked( + onCaptureBitmap, + requireContext(), + ::showShareForFile + ) + } ) } } } } + private fun showShareForFile(file: File) { + val context = requireContext() + try { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "image/png" + val uri = FileUtil.createUriWithReadPermissions(file, intent, requireContext()) + intent.putExtra(Intent.EXTRA_STREAM, uri) + context.startActivity( + Intent.createChooser( + intent, + context.getString(LR.string.end_of_year_share_via) + ) + ) + } catch (e: Exception) { + Timber.e(e) + } + } + companion object { fun newInstance() = StoriesFragment() } diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index 6301e09fa51..2d75ba6e12a 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -1,5 +1,6 @@ package au.com.shiftyjelly.pocketcasts.endofyear +import android.graphics.Bitmap import androidx.annotation.FloatRange import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -43,6 +44,7 @@ import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1 import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake2 +import au.com.shiftyjelly.pocketcasts.endofyear.views.snapShot import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake1View import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake2View import au.com.shiftyjelly.pocketcasts.models.entity.Podcast @@ -56,6 +58,7 @@ private val StoryViewCornerSize = 10.dp fun StoriesScreen( viewModel: StoriesViewModel, onCloseClicked: () -> Unit, + onShareClicked: (() -> Bitmap) -> Unit, ) { val state: State by viewModel.state.collectAsState() val progress: Float by viewModel.progress.collectAsState() @@ -67,8 +70,10 @@ fun StoriesScreen( onSkipNext = { viewModel.skipNext() }, onPause = { viewModel.pause() }, onStart = { viewModel.start() }, - onCloseClicked = onCloseClicked + onCloseClicked = onCloseClicked, + onShareClicked = onShareClicked, ) + State.Loading -> StoriesLoadingView(onCloseClicked) State.Error -> StoriesErrorView(onCloseClicked) } @@ -83,6 +88,7 @@ private fun StoriesView( onPause: () -> Unit, onStart: () -> Unit, onCloseClicked: () -> Unit, + onShareClicked: (() -> Bitmap) -> Unit, modifier: Modifier = Modifier, ) { var screenWidth by remember { mutableStateOf(1) } @@ -120,7 +126,7 @@ private fun StoriesView( } ) { state.currentStory?.let { - StoryView(it) + StoryView(story = it, onShareClicked = onShareClicked) } SegmentedProgressIndicator( progress = progress, @@ -137,27 +143,50 @@ private fun StoriesView( private fun StoryView( story: Story, modifier: Modifier = Modifier, + onShareClicked: (() -> Bitmap) -> Unit, ) { + var onCaptureBitmap: (() -> Bitmap)? = null Column { - Box( + Column( modifier = modifier .fillMaxSize() - .weight(weight = 1f, fill = true) - .clip(RoundedCornerShape(StoryViewCornerSize)) - .background(color = story.backgroundColor), - contentAlignment = Alignment.Center + .weight(weight = 1f, fill = true), ) { - when (story) { - is StoryFake1 -> StoryFake1View(story) - is StoryFake2 -> StoryFake2View(story) + onCaptureBitmap = snapShot( + content = { StorySharableContent(story, modifier) } + ) + } + ShareButton( + onClick = { + onShareClicked.invoke(requireNotNull(onCaptureBitmap)) } + ) + } +} + +@Composable +private fun StorySharableContent( + story: Story, + modifier: Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(StoryViewCornerSize)) + .background(color = story.backgroundColor), + contentAlignment = Alignment.Center + ) { + when (story) { + is StoryFake1 -> StoryFake1View(story) + is StoryFake2 -> StoryFake2View(story) } - ShareButton() } } @Composable -private fun ShareButton() { +private fun ShareButton( + onClick: () -> Unit, +) { RowOutlinedButton( text = stringResource(id = LR.string.share), border = BorderStroke(ShareButtonStrokeWidth, Color.White), @@ -167,7 +196,9 @@ private fun ShareButton() { contentColor = Color.White, ), iconImage = Icons.Default.Share, - onClick = {} + onClick = { + onClick.invoke() + } ) } @@ -263,7 +294,8 @@ private fun StoriesScreenPreview( onSkipNext = {}, onPause = {}, onStart = {}, - onCloseClicked = {} + onCloseClicked = {}, + onShareClicked = {} ) } } diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt index e29825cf865..a5d1ed0c763 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt @@ -1,15 +1,19 @@ package au.com.shiftyjelly.pocketcasts.endofyear +import android.content.Context +import android.graphics.Bitmap import androidx.annotation.FloatRange import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State.Loaded.SegmentsData import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story +import au.com.shiftyjelly.pocketcasts.utils.FileUtilWrapper import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import timber.log.Timber +import java.io.File import java.util.Timer import javax.inject.Inject import kotlin.concurrent.fixedRateTimer @@ -18,6 +22,7 @@ import kotlin.math.roundToInt @HiltViewModel class StoriesViewModel @Inject constructor( private val storiesDataSource: StoriesDataSource, + private val fileUtilWrapper: FileUtilWrapper, ) : ViewModel() { private val mutableState = MutableStateFlow(State.Loading) val state: StateFlow = mutableState @@ -121,6 +126,20 @@ class StoriesViewModel @Inject constructor( return (sumOfStoryLengthsTillIndex + StoriesDataSource.STORY_GAP_LENGTH_MS * index) / storiesDataSource.totalLengthInMs.toFloat() } + fun onShareClicked( + onCaptureBitmap: () -> Bitmap, + context: Context, + showShareForFile: (File) -> Unit + ) = viewModelScope.launch { + val savedFile = fileUtilWrapper.saveBitmapToFile( + onCaptureBitmap.invoke(), + context, + EOY_STORY_SAVE_FOLDER_NAME, + EOY_STORY_SAVE_FILE_NAME + ) + savedFile?.let { showShareForFile.invoke(it) } + } + sealed class State { object Loading : State() data class Loaded( @@ -132,6 +151,7 @@ class StoriesViewModel @Inject constructor( val xStartOffsets: List = emptyList(), ) } + object Error : State() } @@ -139,5 +159,7 @@ class StoriesViewModel @Inject constructor( private const val PROGRESS_START_VALUE = 0f private const val PROGRESS_END_VALUE = 1f private const val PROGRESS_UPDATE_INTERVAL_MS = 10L + private const val EOY_STORY_SAVE_FOLDER_NAME = "eoy_images_cache" + private const val EOY_STORY_SAVE_FILE_NAME = "eoy_shared_image.png" } } diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt new file mode 100644 index 00000000000..50a2e7284f5 --- /dev/null +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt @@ -0,0 +1,35 @@ +package au.com.shiftyjelly.pocketcasts.endofyear.views + +import android.graphics.Bitmap +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.drawToBitmap + +/* Returns a callback to get latest composable bitmap. +Credits: https://rb.gy/g5vuez */ +@Composable +fun snapShot( + content: @Composable () -> Unit, +): () -> Bitmap { + val context = LocalContext.current + val composeView = remember { ComposeView(context) } + + fun onCaptureBitmap(): Bitmap { + return composeView.drawToBitmap() + } + + AndroidView( + factory = { + composeView.apply { + setContent { + content.invoke() + } + } + } + ) + + return ::onCaptureBitmap +} diff --git a/modules/services/localization/src/main/res/values/strings.xml b/modules/services/localization/src/main/res/values/strings.xml index b8533ee3153..cc3d64f86d1 100644 --- a/modules/services/localization/src/main/res/values/strings.xml +++ b/modules/services/localization/src/main/res/values/strings.xml @@ -1388,6 +1388,7 @@ Not Now @string/end_of_year_launch_modal_title See your top podcasts, categories, listening stats and more. + @string/podcasts_share_via diff --git a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt index 7ff547644dd..4d9c6e7772f 100644 --- a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt +++ b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt @@ -16,7 +16,7 @@ class FileUtilWrapper @Inject constructor() { } suspend fun saveBitmapToFile( - image: Bitmap, + bitmap: Bitmap, context: Context, saveFolderName: String, saveFileName: String, @@ -27,7 +27,7 @@ class FileUtilWrapper @Inject constructor() { imagesFolder.mkdirs() file = File(imagesFolder, saveFileName) val stream = FileOutputStream(file) - image.compress(Bitmap.CompressFormat.PNG, 90, stream) + bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) stream.flush() stream.close() } catch (e: IOException) { From 1e7e6b315ac32a74fbaa06cf3a3d808cbdaf398a Mon Sep 17 00:00:00 2001 From: ashiagr Date: Fri, 28 Oct 2022 14:04:53 +0530 Subject: [PATCH 04/13] Fix existing tests --- .../endofyear/StoriesViewModelTest.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt index b0636d7d5da..773ae02de27 100644 --- a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt +++ b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt @@ -1,6 +1,7 @@ package au.com.shiftyjelly.pocketcasts.endofyear import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story +import au.com.shiftyjelly.pocketcasts.utils.FileUtilWrapper import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -13,6 +14,7 @@ import kotlinx.coroutines.test.setMain import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -22,6 +24,8 @@ private val story2 = mock() @RunWith(MockitoJUnitRunner::class) class StoriesViewModelTest { + @Mock + private lateinit var fileUtilWrapper: FileUtilWrapper @OptIn(ExperimentalCoroutinesApi::class) @Before @@ -33,7 +37,7 @@ class StoriesViewModelTest { @Test fun `when vm starts, then loading is shown`() = runTest { Dispatchers.setMain(StandardTestDispatcher()) - val viewModel = StoriesViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) assertEquals(viewModel.state.value is StoriesViewModel.State.Loading, true) } @@ -42,7 +46,7 @@ class StoriesViewModelTest { @Test fun `when vm starts, then progress is zero`() = runTest { Dispatchers.setMain(StandardTestDispatcher()) - val viewModel = StoriesViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) assertEquals(viewModel.progress.value, 0f) } @@ -51,28 +55,28 @@ class StoriesViewModelTest { @Test fun `when vm starts, then stories are loaded`() = runTest { val dataSource = mock() - StoriesViewModel(dataSource) + initViewModel(dataSource) verify(dataSource).loadStories() } @Test fun `given no stories found, when vm starts, then error is shown`() { - val viewModel = StoriesViewModel(MockStoriesDataSource(emptyList())) + val viewModel = initViewModel(MockStoriesDataSource(emptyList())) assertEquals(viewModel.state.value is StoriesViewModel.State.Error, true) } @Test fun `given stories found, when vm starts, then screen is loaded`() { - val viewModel = StoriesViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) assertEquals(viewModel.state.value is StoriesViewModel.State.Loaded, true) } @Test fun `when next is invoked, then next story is shown`() { - val viewModel = StoriesViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) viewModel.skipNext() @@ -82,7 +86,7 @@ class StoriesViewModelTest { @Test fun `when previous is invoked, then previous story is shown`() { - val viewModel = StoriesViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) viewModel.skipNext() viewModel.skipPrevious() @@ -91,6 +95,11 @@ class StoriesViewModelTest { assertEquals(state.currentStory, story1) } + private fun initViewModel(storiesDataSource: StoriesDataSource) = StoriesViewModel( + storiesDataSource = storiesDataSource, + fileUtilWrapper = fileUtilWrapper + ) + class MockStoriesDataSource(private val mockStories: List) : StoriesDataSource() { override val stories = mutableListOf() From 5f6c05be9bcdeb16a3b4ce14bde2c78e56e21639 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Fri, 28 Oct 2022 14:17:13 +0530 Subject: [PATCH 05/13] Pause/ play story when sharing is shown / dismissed --- .../pocketcasts/endofyear/StoriesFragment.kt | 13 +++++++------ .../pocketcasts/endofyear/StoriesViewModel.kt | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt index db84bbbd8c1..18351b6a817 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.viewModels @@ -23,6 +24,11 @@ class StoriesFragment : BaseDialogFragment() { override val statusBarColor: StatusBarColor get() = StatusBarColor.Custom(Color.BLACK, true) + private val shareLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + /* Share activity dismissed, start paused story */ + viewModel.start() + } + override fun onCreate(savedInstance: Bundle?) { super.onCreate(savedInstance) setStyle(STYLE_NORMAL, R.style.BottomSheetDialogThemeBlack) @@ -60,12 +66,7 @@ class StoriesFragment : BaseDialogFragment() { intent.type = "image/png" val uri = FileUtil.createUriWithReadPermissions(file, intent, requireContext()) intent.putExtra(Intent.EXTRA_STREAM, uri) - context.startActivity( - Intent.createChooser( - intent, - context.getString(LR.string.end_of_year_share_via) - ) - ) + shareLauncher.launch(Intent.createChooser(intent, context.getString(LR.string.end_of_year_share_via))) } catch (e: Exception) { Timber.e(e) } diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt index a5d1ed0c763..ba405fa1ba1 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt @@ -130,14 +130,17 @@ class StoriesViewModel @Inject constructor( onCaptureBitmap: () -> Bitmap, context: Context, showShareForFile: (File) -> Unit - ) = viewModelScope.launch { - val savedFile = fileUtilWrapper.saveBitmapToFile( - onCaptureBitmap.invoke(), - context, - EOY_STORY_SAVE_FOLDER_NAME, - EOY_STORY_SAVE_FILE_NAME - ) - savedFile?.let { showShareForFile.invoke(it) } + ) { + pause() + viewModelScope.launch { + val savedFile = fileUtilWrapper.saveBitmapToFile( + onCaptureBitmap.invoke(), + context, + EOY_STORY_SAVE_FOLDER_NAME, + EOY_STORY_SAVE_FILE_NAME + ) + savedFile?.let { showShareForFile.invoke(it) } + } } sealed class State { From 94276c73ccee938847ae3533ffe7693c595cade2 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Sun, 30 Oct 2022 07:10:55 +0530 Subject: [PATCH 06/13] Attempt to fix weird duplicate stories state Use stateIn on the resulting flow returned by combine https://rb.gy/ew2iqy --- .../endofyear/StoriesDataSource.kt | 20 +----- .../pocketcasts/endofyear/StoriesViewModel.kt | 41 ++++++----- .../stories/EndOfYearStoriesDataSource.kt | 18 ++--- .../endofyear/StoriesViewModelTest.kt | 69 ++++++++----------- 4 files changed, 60 insertions(+), 88 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesDataSource.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesDataSource.kt index 8cb0b6c5240..f1a04f7162c 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesDataSource.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesDataSource.kt @@ -3,22 +3,6 @@ package au.com.shiftyjelly.pocketcasts.endofyear import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story import kotlinx.coroutines.flow.Flow -abstract class StoriesDataSource { - protected abstract val stories: List - - val numOfStories: Int - get() = stories.size - val totalLengthInMs - get() = storyLengthsInMs.sum() + gapLengthsInMs - val storyLengthsInMs: List - get() = stories.map { it.storyLength } - private val gapLengthsInMs: Long - get() = STORY_GAP_LENGTH_MS * numOfStories.minus(1).coerceAtLeast(0) - - abstract suspend fun loadStories(): Flow> - abstract fun storyAt(index: Int): Story? - - companion object { - const val STORY_GAP_LENGTH_MS = 100L - } +interface StoriesDataSource { + suspend fun loadStories(): Flow> } diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt index ba405fa1ba1..d88a4b5c625 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt @@ -11,6 +11,7 @@ import au.com.shiftyjelly.pocketcasts.utils.FileUtilWrapper import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber import java.io.File @@ -30,30 +31,37 @@ class StoriesViewModel @Inject constructor( private val mutableProgress = MutableStateFlow(0f) val progress: StateFlow = mutableProgress + private val stories = mutableListOf() private val numOfStories: Int - get() = storiesDataSource.numOfStories + get() = stories.size private var currentIndex: Int = 0 private val nextIndex get() = (currentIndex.plus(1)).coerceAtMost(numOfStories.minus(1)) + private val totalLengthInMs + get() = storyLengthsInMs.sum() + gapLengthsInMs + private val storyLengthsInMs: List + get() = stories.map { it.storyLength } + private val gapLengthsInMs: Long + get() = STORY_GAP_LENGTH_MS * numOfStories.minus(1).coerceAtLeast(0) private var timer: Timer? = null init { viewModelScope.launch { - storiesDataSource.loadStories().collect { stories -> - val state = if (stories.isEmpty()) { + storiesDataSource.loadStories().stateIn(viewModelScope).collect { result -> + stories.clear() + val state = if (result.isEmpty()) { State.Error } else { - with(storiesDataSource) { - State.Loaded( - currentStory = storyAt(currentIndex), - segmentsData = SegmentsData( - xStartOffsets = List(numOfStories) { getXStartOffsetAtIndex(it) }, - widths = storyLengthsInMs.map { it / totalLengthInMs.toFloat() }, - ) + stories.addAll(result) + State.Loaded( + currentStory = result[currentIndex], + segmentsData = SegmentsData( + xStartOffsets = List(numOfStories) { getXStartOffsetAtIndex(it) }, + widths = storyLengthsInMs.map { it / totalLengthInMs.toFloat() }, ) - } + ) } mutableState.value = state if (state is State.Loaded) start() @@ -64,7 +72,7 @@ class StoriesViewModel @Inject constructor( fun start() { val currentState = state.value as State.Loaded val progressFraction = - (PROGRESS_UPDATE_INTERVAL_MS / storiesDataSource.totalLengthInMs.toFloat()) + (PROGRESS_UPDATE_INTERVAL_MS / totalLengthInMs.toFloat()) .coerceAtMost(PROGRESS_END_VALUE) timer = fixedRateTimer(period = PROGRESS_UPDATE_INTERVAL_MS) { @@ -74,7 +82,7 @@ class StoriesViewModel @Inject constructor( if (newProgress.roundOff() == getXStartOffsetAtIndex(nextIndex).roundOff()) { currentIndex = nextIndex mutableState.value = - currentState.copy(currentStory = storiesDataSource.storyAt(currentIndex)) + currentState.copy(currentStory = stories[currentIndex]) } mutableProgress.value = newProgress @@ -100,7 +108,7 @@ class StoriesViewModel @Inject constructor( mutableProgress.value = getXStartOffsetAtIndex(index) currentIndex = index mutableState.value = - (state.value as State.Loaded).copy(currentStory = storiesDataSource.storyAt(index)) + (state.value as State.Loaded).copy(currentStory = stories[index]) } private fun cancelTimer() { @@ -118,12 +126,12 @@ class StoriesViewModel @Inject constructor( @FloatRange(from = 0.0, to = 1.0) fun getXStartOffsetAtIndex(index: Int): Float { val sumOfStoryLengthsTillIndex = try { - storiesDataSource.storyLengthsInMs.subList(0, index).sum() + storyLengthsInMs.subList(0, index).sum() } catch (e: IndexOutOfBoundsException) { Timber.e("Story offset checked at invalid index") 0L } - return (sumOfStoryLengthsTillIndex + StoriesDataSource.STORY_GAP_LENGTH_MS * index) / storiesDataSource.totalLengthInMs.toFloat() + return (sumOfStoryLengthsTillIndex + STORY_GAP_LENGTH_MS * index) / totalLengthInMs.toFloat() } fun onShareClicked( @@ -159,6 +167,7 @@ class StoriesViewModel @Inject constructor( } companion object { + private const val STORY_GAP_LENGTH_MS = 100L private const val PROGRESS_START_VALUE = 0f private const val PROGRESS_END_VALUE = 1f private const val PROGRESS_UPDATE_INTERVAL_MS = 10L diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/EndOfYearStoriesDataSource.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/EndOfYearStoriesDataSource.kt index 1ebe1b6f572..d0112adbe07 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/EndOfYearStoriesDataSource.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/EndOfYearStoriesDataSource.kt @@ -2,30 +2,24 @@ package au.com.shiftyjelly.pocketcasts.endofyear.stories import au.com.shiftyjelly.pocketcasts.endofyear.StoriesDataSource import au.com.shiftyjelly.pocketcasts.repositories.podcast.EndOfYearManager +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import timber.log.Timber import javax.inject.Inject class EndOfYearStoriesDataSource @Inject constructor( private val endOfYearManager: EndOfYearManager, -) : StoriesDataSource() { - override val stories = mutableListOf() - - override suspend fun loadStories() = - combine( +) : StoriesDataSource { + override suspend fun loadStories(): Flow> { + return combine( endOfYearManager.findRandomPodcasts(), endOfYearManager.findRandomEpisode() ) { podcasts, episode -> + val stories = mutableListOf() + if (podcasts.isNotEmpty()) stories.add(StoryFake1(podcasts)) episode?.let { stories.add(StoryFake2(it)) } stories } - - override fun storyAt(index: Int) = try { - stories[index] - } catch (e: IndexOutOfBoundsException) { - Timber.e("Story index is out of bounds") - null } } diff --git a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt index 773ae02de27..9c359cf6a76 100644 --- a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt +++ b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt @@ -5,7 +5,6 @@ import au.com.shiftyjelly.pocketcasts.utils.FileUtilWrapper import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -18,65 +17,65 @@ import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever private val story1 = mock() private val story2 = mock() +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(MockitoJUnitRunner::class) class StoriesViewModelTest { @Mock private lateinit var fileUtilWrapper: FileUtilWrapper - @OptIn(ExperimentalCoroutinesApi::class) + @Mock + private lateinit var storiesDataSource: StoriesDataSource + @Before fun setUp() { Dispatchers.setMain(UnconfinedTestDispatcher()) } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `when vm starts, then loading is shown`() = runTest { + fun `when vm starts, then progress is zero`() = runTest { Dispatchers.setMain(StandardTestDispatcher()) - val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(listOf(story1, story2)) - assertEquals(viewModel.state.value is StoriesViewModel.State.Loading, true) + assertEquals(viewModel.progress.value, 0f) } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `when vm starts, then progress is zero`() = runTest { + fun `when vm starts, then loading is shown`() = runTest { Dispatchers.setMain(StandardTestDispatcher()) - val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) + val viewModel = initViewModel(listOf(story1, story2)) - assertEquals(viewModel.progress.value, 0f) + assertEquals(viewModel.state.value is StoriesViewModel.State.Loading, true) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `when vm starts, then stories are loaded`() = runTest { - val dataSource = mock() - initViewModel(dataSource) + initViewModel(emptyList()) - verify(dataSource).loadStories() + verify(storiesDataSource).loadStories() } @Test - fun `given no stories found, when vm starts, then error is shown`() { - val viewModel = initViewModel(MockStoriesDataSource(emptyList())) + fun `given no stories found, when vm starts, then error is shown`() = runTest { + val viewModel = initViewModel(emptyList()) assertEquals(viewModel.state.value is StoriesViewModel.State.Error, true) } @Test - fun `given stories found, when vm starts, then screen is loaded`() { - val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) + fun `given stories found, when vm starts, then screen is loaded`() = runTest { + val viewModel = initViewModel(listOf(story1, story2)) assertEquals(viewModel.state.value is StoriesViewModel.State.Loaded, true) } @Test - fun `when next is invoked, then next story is shown`() { - val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) + fun `when next is invoked, then next story is shown`() = runTest { + val viewModel = initViewModel(listOf(story1, story2)) viewModel.skipNext() @@ -85,8 +84,8 @@ class StoriesViewModelTest { } @Test - fun `when previous is invoked, then previous story is shown`() { - val viewModel = initViewModel(MockStoriesDataSource(listOf(story1, story2))) + fun `when previous is invoked, then previous story is shown`() = runTest { + val viewModel = initViewModel(listOf(story1, story2)) viewModel.skipNext() viewModel.skipPrevious() @@ -95,25 +94,11 @@ class StoriesViewModelTest { assertEquals(state.currentStory, story1) } - private fun initViewModel(storiesDataSource: StoriesDataSource) = StoriesViewModel( - storiesDataSource = storiesDataSource, - fileUtilWrapper = fileUtilWrapper - ) - - class MockStoriesDataSource(private val mockStories: List) : StoriesDataSource() { - override val stories = mutableListOf() - - override suspend fun loadStories(): Flow> { - stories.addAll(mockStories) - return flowOf(stories) - } - - override fun storyAt(index: Int): Story? { - return try { - stories[index] - } catch (e: IndexOutOfBoundsException) { - null - } - } + private suspend fun initViewModel(mockStories: List): StoriesViewModel { + whenever(storiesDataSource.loadStories()).thenReturn(flowOf(mockStories)) + return StoriesViewModel( + storiesDataSource = storiesDataSource, + fileUtilWrapper = fileUtilWrapper + ) } } From 052069b0e5708c33a2616677304b3b3ce79567f0 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Mon, 31 Oct 2022 09:39:52 +0530 Subject: [PATCH 07/13] Introduce isInteractive story If true - StoryView is a child of StorySwitcher, all inputs to interactive elements of StoryView are consumed by the elements and other inputs are passed to the parent. If false - StorySwitcher is laid over StoryView and it takes all inputs. --- .../pocketcasts/endofyear/StoriesScreen.kt | 131 ++++++++++-------- .../pocketcasts/endofyear/stories/Story.kt | 1 + 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index 2d75ba6e12a..e06ff6a27dd 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -54,6 +54,7 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR private val ShareButtonStrokeWidth = 2.dp private val StoryViewCornerSize = 10.dp +private var onCaptureBitmap: (() -> Bitmap)? = null @Composable fun StoriesScreen( viewModel: StoriesViewModel, @@ -91,51 +92,32 @@ private fun StoriesView( onShareClicked: (() -> Bitmap) -> Unit, modifier: Modifier = Modifier, ) { - var screenWidth by remember { mutableStateOf(1) } - var isPaused by remember { mutableStateOf(false) } - Box( - modifier = modifier - .fillMaxSize() - .background(color = Color.Black) - .onGloballyPositioned { - screenWidth = it.size.width - } - .pointerInput(Unit) { - detectTapGestures( - onTap = { - if (!isPaused) { - if (it.x > screenWidth / 2) { - onSkipNext() - } else { - onSkipPrevious() - } - } - }, - onLongPress = { - isPaused = true - onPause() - }, - onPress = { - awaitRelease() - if (isPaused) { - onStart() - isPaused = false - } - } + Column(modifier = modifier.background(color = Color.Black)) { + Box(modifier = modifier.weight(weight = 1f, fill = true)) { + state.currentStory?.let { + if (!it.isInteractive) StoryView(story = it) + StorySwitcher( + onSkipPrevious = onSkipPrevious, + onSkipNext = onSkipNext, + onPause = onPause, + onStart = onStart, + content = { if (it.isInteractive) StoryView(story = it) } ) } - ) { - state.currentStory?.let { - StoryView(story = it, onShareClicked = onShareClicked) + SegmentedProgressIndicator( + progress = progress, + segmentsData = state.segmentsData, + modifier = modifier + .padding(8.dp) + .fillMaxWidth(), + ) + CloseButtonView(onCloseClicked) } - SegmentedProgressIndicator( - progress = progress, - segmentsData = state.segmentsData, - modifier = modifier - .padding(8.dp) - .fillMaxWidth(), + ShareButton( + onClick = { + onShareClicked.invoke(requireNotNull(onCaptureBitmap)) + } ) - CloseButtonView(onCloseClicked) } } @@ -143,25 +125,10 @@ private fun StoriesView( private fun StoryView( story: Story, modifier: Modifier = Modifier, - onShareClicked: (() -> Bitmap) -> Unit, ) { - var onCaptureBitmap: (() -> Bitmap)? = null - Column { - Column( - modifier = modifier - .fillMaxSize() - .weight(weight = 1f, fill = true), - ) { - onCaptureBitmap = snapShot( - content = { StorySharableContent(story, modifier) } - ) - } - ShareButton( - onClick = { - onShareClicked.invoke(requireNotNull(onCaptureBitmap)) - } - ) - } + onCaptureBitmap = snapShot( + content = { StorySharableContent(story, modifier) } + ) } @Composable @@ -275,6 +242,52 @@ private fun StoriesEmptyView( } } +@Composable +private fun StorySwitcher( + onSkipPrevious: () -> Unit, + onSkipNext: () -> Unit, + onPause: () -> Unit, + onStart: () -> Unit, + modifier: Modifier = Modifier, + content: (@Composable () -> Unit)?, +) { + var screenWidth by remember { mutableStateOf(1) } + var isPaused by remember { mutableStateOf(false) } + Box( + modifier = modifier + .fillMaxSize() + .onGloballyPositioned { + screenWidth = it.size.width + } + .pointerInput(Unit) { + detectTapGestures( + onTap = { + if (!isPaused) { + if (it.x > screenWidth / 2) { + onSkipNext() + } else { + onSkipPrevious() + } + } + }, + onLongPress = { + isPaused = true + onPause() + }, + onPress = { + awaitRelease() + if (isPaused) { + onStart() + isPaused = false + } + } + ) + } + ) { + content?.invoke() + } +} + @Preview(showBackground = true) @Composable private fun StoriesScreenPreview( diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/Story.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/Story.kt index 9deee1f6699..3b250897579 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/Story.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/stories/Story.kt @@ -7,4 +7,5 @@ abstract class Story { open val storyLength: Long = 2.seconds() abstract val backgroundColor: Color val tintColor: Color = Color.White + val isInteractive: Boolean = false } From 90e0a7cc7c6cb81f028949a5375bcb0c30596de9 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Mon, 31 Oct 2022 12:53:54 +0530 Subject: [PATCH 08/13] Update fake story layout Temporary changes to fit layout to screen size --- .../endofyear/views/stories/StoryFake1View.kt | 46 +++++++++++++++++-- .../compose/components/PodcastItem.kt | 6 ++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt index e1a02beb083..28ee802e83a 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/stories/StoryFake1View.kt @@ -1,35 +1,64 @@ package au.com.shiftyjelly.pocketcasts.endofyear.views.stories +import android.content.Context +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import au.com.shiftyjelly.pocketcasts.compose.components.PodcastItem import au.com.shiftyjelly.pocketcasts.compose.components.TextH30 import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1 +import au.com.shiftyjelly.pocketcasts.utils.extensions.pxToDp @Composable fun StoryFake1View( story: StoryFake1, modifier: Modifier = Modifier, ) { - Column { + val context = LocalContext.current + var screenHeight by remember { mutableStateOf(1) } + var textFieldHeight by remember { mutableStateOf(1) } + Column( + modifier = modifier + .fillMaxSize() + .onGloballyPositioned { + screenHeight = it.size.height + }, + verticalArrangement = Arrangement.Center + ) { TextH30( text = "Your Top Podcasts", textAlign = TextAlign.Center, color = story.tintColor, modifier = modifier .fillMaxWidth() - .padding(bottom = 16.dp) + .padding(vertical = 16.dp) + .onGloballyPositioned { + textFieldHeight = it.size.height + } ) - LazyColumn(modifier = modifier.fillMaxWidth()) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Center + ) { items(story.podcasts.size) { index -> PodcastItem( podcast = story.podcasts[index], + iconSize = getIconSize(screenHeight, textFieldHeight, context), onClick = {}, tintColor = story.tintColor, showDivider = false @@ -38,3 +67,14 @@ fun StoryFake1View( } } } + +fun getIconSize( + screenHeight: Int, + textFieldHeight: Int, + context: Context +): Dp { + return screenHeight.pxToDp(context).dp + .minus(32.dp + textFieldHeight.dp) + .div(5) + .coerceAtMost(64.dp) +} diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/PodcastItem.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/PodcastItem.kt index cc56470be88..6e40a224196 100644 --- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/PodcastItem.kt +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/PodcastItem.kt @@ -15,18 +15,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import au.com.shiftyjelly.pocketcasts.compose.theme import au.com.shiftyjelly.pocketcasts.models.entity.Podcast import au.com.shiftyjelly.pocketcasts.images.R as IR import au.com.shiftyjelly.pocketcasts.localization.R as LR +private val PodcastItemIconSize = 64.dp + @Composable fun PodcastItem( podcast: Podcast, onClick: (() -> Unit)?, modifier: Modifier = Modifier, tintColor: Color? = null, + iconSize: Dp = PodcastItemIconSize, subscribed: Boolean = false, showSubscribed: Boolean = false, showDivider: Boolean = true, @@ -42,7 +46,7 @@ fun PodcastItem( Box(modifier = Modifier.padding(top = 4.dp, end = 12.dp, bottom = 4.dp)) { PodcastImage( uuid = podcast.uuid, - modifier = Modifier.size(64.dp) + modifier = Modifier.size(iconSize) ) } Column( From 295eae744bfeaf830cdee05ea653b38677b6b78d Mon Sep 17 00:00:00 2001 From: ashiagr Date: Mon, 31 Oct 2022 17:23:25 +0530 Subject: [PATCH 09/13] Update translatable false --- modules/services/localization/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/services/localization/src/main/res/values/strings.xml b/modules/services/localization/src/main/res/values/strings.xml index cc3d64f86d1..d5d965ecbb2 100644 --- a/modules/services/localization/src/main/res/values/strings.xml +++ b/modules/services/localization/src/main/res/values/strings.xml @@ -1388,7 +1388,7 @@ Not Now @string/end_of_year_launch_modal_title See your top podcasts, categories, listening stats and more. - @string/podcasts_share_via + @string/podcasts_share_via From b42faa61e0d12ee5c603141e927a00d9db34d6c9 Mon Sep 17 00:00:00 2001 From: Ashita Agrawal Date: Tue, 1 Nov 2022 09:55:19 +0530 Subject: [PATCH 10/13] Update modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt Co-authored-by: Matt Chowning --- .../com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt index 4d9c6e7772f..c8ed3c5a710 100644 --- a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt +++ b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/FileUtilWrapper.kt @@ -26,10 +26,10 @@ class FileUtilWrapper @Inject constructor() { try { imagesFolder.mkdirs() file = File(imagesFolder, saveFileName) - val stream = FileOutputStream(file) - bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) - stream.flush() - stream.close() + FileOutputStream(file).use { stream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) + stream.flush() + } } catch (e: IOException) { Timber.e("Error while saving image to file " + e.message) } From cd3609e09fe8e86095605d2982558d690826dfdc Mon Sep 17 00:00:00 2001 From: ashiagr Date: Tue, 1 Nov 2022 10:05:26 +0530 Subject: [PATCH 11/13] Scope onCaptureBitmap to inside StoriesView --- .../pocketcasts/endofyear/StoriesScreen.kt | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index e06ff6a27dd..1d4f26fd95e 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -54,7 +54,6 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR private val ShareButtonStrokeWidth = 2.dp private val StoryViewCornerSize = 10.dp -private var onCaptureBitmap: (() -> Bitmap)? = null @Composable fun StoriesScreen( viewModel: StoriesViewModel, @@ -93,25 +92,32 @@ private fun StoriesView( modifier: Modifier = Modifier, ) { Column(modifier = modifier.background(color = Color.Black)) { - Box(modifier = modifier.weight(weight = 1f, fill = true)) { - state.currentStory?.let { - if (!it.isInteractive) StoryView(story = it) + var onCaptureBitmap: (() -> Bitmap)? = null + state.currentStory?.let { story -> + Box(modifier = modifier.weight(weight = 1f, fill = true)) { + if (!story.isInteractive) { + onCaptureBitmap = snapShot(content = { StorySharableContent(story, modifier) }) + } StorySwitcher( onSkipPrevious = onSkipPrevious, onSkipNext = onSkipNext, onPause = onPause, onStart = onStart, - content = { if (it.isInteractive) StoryView(story = it) } + ) { + if (story.isInteractive) { + onCaptureBitmap = + snapShot(content = { StorySharableContent(story, modifier) }) + } + } + SegmentedProgressIndicator( + progress = progress, + segmentsData = state.segmentsData, + modifier = modifier + .padding(8.dp) + .fillMaxWidth(), ) + CloseButtonView(onCloseClicked) } - SegmentedProgressIndicator( - progress = progress, - segmentsData = state.segmentsData, - modifier = modifier - .padding(8.dp) - .fillMaxWidth(), - ) - CloseButtonView(onCloseClicked) } ShareButton( onClick = { @@ -121,16 +127,6 @@ private fun StoriesView( } } -@Composable -private fun StoryView( - story: Story, - modifier: Modifier = Modifier, -) { - onCaptureBitmap = snapShot( - content = { StorySharableContent(story, modifier) } - ) -} - @Composable private fun StorySharableContent( story: Story, From 4b73333b41b484660bca6c8e3ccff77bb1e0ed03 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Tue, 1 Nov 2022 10:09:59 +0530 Subject: [PATCH 12/13] Move requireNotNull check up the hierarchy to fail faster --- .../shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index 1d4f26fd95e..376ac3a8a90 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -119,11 +119,11 @@ private fun StoriesView( CloseButtonView(onCloseClicked) } } - ShareButton( - onClick = { - onShareClicked.invoke(requireNotNull(onCaptureBitmap)) - } - ) + requireNotNull(onCaptureBitmap).let { + ShareButton( + onClick = { onShareClicked.invoke(it) } + ) + } } } From 22b0d710dc3dda91ad8019116d175c5049e903e0 Mon Sep 17 00:00:00 2001 From: ashiagr Date: Tue, 1 Nov 2022 10:23:59 +0530 Subject: [PATCH 13/13] Rename snapShot, inline callback and add notes --- .../pocketcasts/endofyear/StoriesScreen.kt | 7 ++++--- .../pocketcasts/endofyear/views/SnapShot.kt | 12 +++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt index 376ac3a8a90..01ae823e508 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt @@ -44,7 +44,7 @@ import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1 import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake2 -import au.com.shiftyjelly.pocketcasts.endofyear.views.snapShot +import au.com.shiftyjelly.pocketcasts.endofyear.views.convertibleToBitmap import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake1View import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryFake2View import au.com.shiftyjelly.pocketcasts.models.entity.Podcast @@ -96,7 +96,8 @@ private fun StoriesView( state.currentStory?.let { story -> Box(modifier = modifier.weight(weight = 1f, fill = true)) { if (!story.isInteractive) { - onCaptureBitmap = snapShot(content = { StorySharableContent(story, modifier) }) + onCaptureBitmap = + convertibleToBitmap(content = { StorySharableContent(story, modifier) }) } StorySwitcher( onSkipPrevious = onSkipPrevious, @@ -106,7 +107,7 @@ private fun StoriesView( ) { if (story.isInteractive) { onCaptureBitmap = - snapShot(content = { StorySharableContent(story, modifier) }) + convertibleToBitmap(content = { StorySharableContent(story, modifier) }) } } SegmentedProgressIndicator( diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt index 50a2e7284f5..134a1626bed 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt @@ -8,19 +8,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.drawToBitmap -/* Returns a callback to get latest composable bitmap. +/* Returns a callback to get bitmap for the passed composable. +The composable is converted to ComposeView and laid out into AndroidView otherwise an illegal state exception is thrown: +View needs to be laid out before calling drawToBitmap() Credits: https://rb.gy/g5vuez */ @Composable -fun snapShot( +fun convertibleToBitmap( content: @Composable () -> Unit, ): () -> Bitmap { val context = LocalContext.current val composeView = remember { ComposeView(context) } - fun onCaptureBitmap(): Bitmap { - return composeView.drawToBitmap() - } - AndroidView( factory = { composeView.apply { @@ -31,5 +29,5 @@ fun snapShot( } ) - return ::onCaptureBitmap + return { composeView.drawToBitmap() } }