Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End of Year: Story snapshot and share #505

Merged
merged 13 commits into from
Nov 1, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -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<Story>

val numOfStories: Int
get() = stories.size
val totalLengthInMs
get() = storyLengthsInMs.sum() + gapLengthsInMs
val storyLengthsInMs: List<Long>
get() = stories.map { it.storyLength }
private val gapLengthsInMs: Long
get() = STORY_GAP_LENGTH_MS * numOfStories.minus(1).coerceAtLeast(0)

abstract suspend fun loadStories(): Flow<List<Story>>
abstract fun storyAt(index: Int): Story?

companion object {
const val STORY_GAP_LENGTH_MS = 100L
}
interface StoriesDataSource {
suspend fun loadStories(): Flow<List<Story>>
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
package au.com.shiftyjelly.pocketcasts.endofyear

import android.content.Intent
import android.graphics.Color
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
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()
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)
Expand All @@ -35,12 +46,32 @@ 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)
shareLauncher.launch(Intent.createChooser(intent, context.getString(LR.string.end_of_year_share_via)))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This launches share intent with an edit button. On clicking "edit" from the chooser, an exception is seen in logcat:

Writing exception to parcel
java.lang.SecurityException: Permission Denial: writing androidx.core.content.FileProvider uri content://<path > requires the provider be exported, or grantUriPermission()

I tried to grant FLAG_GRANT_WRITE_URI_PERMISSION locally but it didn't help. Strangely, testing on an emulator shows logs even for reading the file. Also see related SO discussions link 1, link2.

Sharing and editing work without problems. I still have to see why these logs are shown. Let me know if you have any suggestions. Another option is to not show the edit button, the share dialog will look this way then:

} catch (e: Exception) {
Timber.e(e)
}
}

companion object {
fun newInstance() = StoriesFragment()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,8 +44,9 @@ 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.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
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.localization.R as LR
Expand All @@ -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()
Expand All @@ -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)
}
Expand All @@ -83,81 +88,69 @@ private fun StoriesView(
onPause: () -> Unit,
onStart: () -> Unit,
onCloseClicked: () -> Unit,
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)) {
var onCaptureBitmap: (() -> Bitmap)? = null
state.currentStory?.let { story ->
Box(modifier = modifier.weight(weight = 1f, fill = true)) {
if (!story.isInteractive) {
onCaptureBitmap =
convertibleToBitmap(content = { StorySharableContent(story, modifier) })
}
StorySwitcher(
onSkipPrevious = onSkipPrevious,
onSkipNext = onSkipNext,
onPause = onPause,
onStart = onStart,
) {
if (story.isInteractive) {
onCaptureBitmap =
convertibleToBitmap(content = { StorySharableContent(story, modifier) })
}
}
SegmentedProgressIndicator(
progress = progress,
segmentsData = state.segmentsData,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
)
CloseButtonView(onCloseClicked)
}
) {
state.currentStory?.let {
StoryView(it)
}
SegmentedProgressIndicator(
progress = progress,
segmentsData = state.segmentsData,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
)
CloseButtonView(onCloseClicked)
requireNotNull(onCaptureBitmap).let {
ShareButton(
onClick = { onShareClicked.invoke(it) }
)
}
}
}

@Composable
private fun StoryView(
private fun StorySharableContent(
story: Story,
modifier: Modifier = Modifier,
modifier: Modifier,
) {
Column {
Box(
modifier = modifier
.fillMaxSize()
.weight(weight = 1f, fill = true)
.clip(RoundedCornerShape(StoryViewCornerSize))
.background(color = story.backgroundColor),
contentAlignment = Alignment.Center
) {
when (story) {
is StoryFake1 -> StoryFake1View(story)
is StoryFake2 -> StoryFake2View(story)
}
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),
Expand All @@ -167,7 +160,9 @@ private fun ShareButton() {
contentColor = Color.White,
),
iconImage = Icons.Default.Share,
onClick = {}
onClick = {
onClick.invoke()
}
)
}

Expand Down Expand Up @@ -244,6 +239,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(
Expand All @@ -263,7 +304,8 @@ private fun StoriesScreenPreview(
onSkipNext = {},
onPause = {},
onStart = {},
onCloseClicked = {}
onCloseClicked = {},
onShareClicked = {}
)
}
}
Expand Down
Loading