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

Add watch settings screen with metered data warning #860

Merged
merged 3 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions modules/services/images/src/main/res/drawable/signin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.793,7.293C10.183,6.902 10.817,6.902 11.207,7.293L15.207,11.293L15.914,12L15.207,12.707L11.207,16.707C10.817,17.098 10.183,17.098 9.793,16.707C9.402,16.317 9.402,15.683 9.793,15.293L12.086,13H3C2.448,13 2,12.552 2,12C2,11.448 2.448,11 3,11H12.086L9.793,8.707C9.402,8.317 9.402,7.683 9.793,7.293Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M6,5C6,3.895 6.895,3 8,3H17C18.105,3 19,3.895 19,5V19C19,20.105 18.105,21 17,21H8C6.895,21 6,20.105 6,19V18C6,17.448 6.448,17 7,17C7.552,17 8,17.448 8,18V19H17V5H8V6C8,6.552 7.552,7 7,7C6.448,7 6,6.552 6,6V5Z"
android:strokeAlpha="0.5"
android:fillColor="#000000"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
</vector>
5 changes: 5 additions & 0 deletions modules/services/localization/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<string name="empty" translatable="false" />

<string name="account">Account</string>
<string name="add_to_up_next">Add to Up Next</string>
<string name="archive">Archive</string>
<string name="unarchive">Unarchive</string>
Expand All @@ -25,6 +26,8 @@
<string name="go_to_podcast">Go to podcast</string>
<string name="go_to_files">Go to files</string>
<string name="in_progress">In progress</string>
<string name="log_in">Log in</string>
<string name="log_out">Log out</string>
<string name="mark_as_played">Mark as played</string>
<string name="mark_as_unplayed">Mark as unplayed</string>
<string name="mark_played">Mark played</string>
Expand Down Expand Up @@ -192,6 +195,7 @@
<string name="stream_warning_title_not_wifi" translatable="false">@string/youre_not_on_wifi</string>
<string name="stream_warning_title_metered_wifi" translatable="false">@string/youre_on_metered_wifi</string>
<string name="stream_warning_summary">Streaming this episode will use data.</string>
<string name="stream_warning_summary_short">Streaming will use data. Proceed?</string>
<string name="stream_warning_button">Stream Anyway</string>

<string name="search_no_podcasts_found">No podcasts found</string>
Expand Down Expand Up @@ -1054,6 +1058,7 @@
<string name="settings_media_notification_controls_title_play_next" translatable="false">@string/play_next</string>
<string name="settings_media_notification_controls_title_playback_speed" translatable="false">@string/playback_speed</string>
<string name="settings_media_notification_controls_title_star" translatable="false">@string/star</string>
<string name="settings_metered_data_warning">Metered data warning</string>
<string name="settings_no_thanks">No thanks</string>
<string name="settings_notifications_new_episodes">New Episodes</string>
<string name="settings_notification_actions">Actions</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,13 @@ open class PlaybackManager @Inject constructor(

episodeSubscription?.dispose()
if (!episode.isDownloaded) {
if (!Util.isCarUiMode(application) && settings.warnOnMeteredNetwork() && !Network.isUnmeteredConnection(application) && !forceStream && play) {
if (!Util.isCarUiMode(application) &&
!Util.isWearOs(application) && // The watch handles these warnings before this is called
settings.warnOnMeteredNetwork() &&
!Network.isUnmeteredConnection(application) &&
!forceStream &&
play
) {
sendDataWarningNotification(episode)
val previousPlaybackState = playbackStateRelay.blockingFirst()
val playbackState = PlaybackState(
Expand Down
9 changes: 9 additions & 0 deletions wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<uses-feature android:name="android.hardware.type.watch" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme
import au.com.shiftyjelly.pocketcasts.wear.ui.FilesScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.FiltersScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.SettingsScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.UpNextScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.WatchListScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.authenticationGraph
import au.com.shiftyjelly.pocketcasts.wear.ui.authenticationSubGraph
import au.com.shiftyjelly.pocketcasts.wear.ui.downloads.DownloadsScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.episode.EpisodeScreenFlow
import au.com.shiftyjelly.pocketcasts.wear.ui.episode.EpisodeScreenFlow.episodeGraph
import au.com.shiftyjelly.pocketcasts.wear.ui.player.NowPlayingScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.player.NowPlayingViewModel
import au.com.shiftyjelly.pocketcasts.wear.ui.player.StreamingConfirmationScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.podcast.PodcastScreen
import au.com.shiftyjelly.pocketcasts.wear.ui.podcasts.PodcastsScreen
import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel
Expand Down Expand Up @@ -59,12 +66,41 @@ fun WearApp(themeType: Theme.ThemeType) {
WatchListScreen(navController::navigate, it.scrollableState)
}

composable(NowPlayingScreen.route) { viewModel ->
viewModel.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off
composable(NowPlayingScreen.route) {
it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off

// Listen for results from streaming confirmation screen
navController.currentBackStackEntry?.savedStateHandle
?.getStateFlow<StreamingConfirmationScreen.Result?>(StreamingConfirmationScreen.resultKey, null)
?.collectAsStateWithLifecycle()?.value?.let { streamingConfirmationResult ->
val viewModel = hiltViewModel<NowPlayingViewModel>()
LaunchedEffect(streamingConfirmationResult) {
viewModel.onStreamingConfirmationResult(streamingConfirmationResult)
// Clear result once consumed
navController.currentBackStackEntry?.savedStateHandle
?.remove<StreamingConfirmationScreen.Result?>(StreamingConfirmationScreen.resultKey)
}
}
Comment on lines +72 to +83
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't love the boilerplate this requires to get a result from the stream warning screen via the savedStateHandle. This seemed like the best solution though since I can't use a shared view model due to the fact that I need different behavior depending on whether the stream warning is showing from the now playing screen or the episode details screen (now playing plays the currently queued episode, epsidoe details queues and plays the currently viewed episode).


NowPlayingScreen(
navigateToEpisode = { episodeUuid ->
navController.navigate(EpisodeScreenFlow.navigateRoute(episodeUuid))
},
showStreamingConfirmation = { navController.navigate(StreamingConfirmationScreen.route) },
)
}

composable(StreamingConfirmationScreen.route) {
it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off

StreamingConfirmationScreen(
onFinished = { result ->
navController.previousBackStackEntry?.savedStateHandle?.set(
StreamingConfirmationScreen.resultKey,
result
)
Comment on lines +98 to +101
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 is passing the result of what the user selected back to the previous screen.

navController.popBackStack()
},
)
}

Expand Down Expand Up @@ -126,6 +162,13 @@ fun WearApp(themeType: Theme.ThemeType) {

composable(FilesScreen.route) { FilesScreen() }

scrollable(SettingsScreen.route) {
SettingsScreen(
scrollState = it.columnState,
signInClick = { navController.navigate(authenticationSubGraph) },
)
}

authenticationGraph(navController)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import androidx.compose.ui.graphics.Color
object WearColors {
val FF202124 = Color(0xFF202124)
val FFA1E7B0 = Color(0xFFA1E7B0)
val FFBDC1C6 = Color(0xFFBDC1C6)
val FFDADCE0 = Color(0xFFDADCE0)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package au.com.shiftyjelly.pocketcasts.wear.ui

import android.content.res.Configuration
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme
import au.com.shiftyjelly.pocketcasts.wear.theme.theme
import au.com.shiftyjelly.pocketcasts.wear.ui.component.ChipScreenHeader
import au.com.shiftyjelly.pocketcasts.wear.ui.component.ChipSectionHeader
import au.com.shiftyjelly.pocketcasts.wear.ui.component.WatchListChip
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import au.com.shiftyjelly.pocketcasts.images.R as IR
import au.com.shiftyjelly.pocketcasts.localization.R as LR

object SettingsScreen {
const val route = "settings_screen"
}

@Composable
fun SettingsScreen(
scrollState: ScalingLazyColumnState,
signInClick: () -> Unit,
) {

val viewModel = hiltViewModel<SettingsViewModel>()
val state by viewModel.state.collectAsState()

Content(
scrollState = scrollState,
state = state,
onWarnOnMeteredChanged = { viewModel.setWarnOnMeteredNetwork(it) },
signInClick = signInClick,
onSignOutClicked = viewModel::signOut,
)
}

@Composable
private fun Content(
scrollState: ScalingLazyColumnState,
state: SettingsViewModel.State,
onWarnOnMeteredChanged: (Boolean) -> Unit,
signInClick: () -> Unit,
onSignOutClicked: () -> Unit,
) {
ScalingLazyColumn(columnState = scrollState) {

item {
ChipScreenHeader(LR.string.settings)
}

item {
ToggleChip(
label = stringResource(LR.string.settings_metered_data_warning),
checked = state.showDataWarning,
onCheckedChanged = onWarnOnMeteredChanged,
)
}

item {
ChipSectionHeader(LR.string.account)
}

item {
val signInState = state.signInState
when (signInState) {
is SignInState.SignedIn -> {
WatchListChip(
titleRes = LR.string.log_out,
secondaryLabel = signInState.email,
iconRes = IR.drawable.ic_signout,
onClick = onSignOutClicked,
)
}
is SignInState.SignedOut -> {
WatchListChip(
titleRes = LR.string.log_in,
iconRes = IR.drawable.signin,
onClick = signInClick,
)
}
}
}
}
}

@Composable
private fun ToggleChip(
label: String,
checked: Boolean,
onCheckedChanged: (Boolean) -> Unit,
) {
val color = MaterialTheme.theme.colors.support05
ToggleChip(
checked = checked,
onCheckedChange = { onCheckedChanged(it) },
label = {
Text(
text = label,
style = MaterialTheme.typography.button,
)
},
toggleControl = {
Icon(
imageVector = ToggleChipDefaults.switchIcon(checked),
contentDescription = stringResource(if (checked) LR.string.on else LR.string.off),
modifier = Modifier
)
},
colors = ToggleChipDefaults.toggleChipColors(
checkedEndBackgroundColor = color.copy(alpha = 0.32f),
checkedToggleControlColor = color,
),
modifier = Modifier.fillMaxWidth()
)
}

@Preview(
widthDp = 200,
heightDp = 200,
uiMode = Configuration.UI_MODE_TYPE_WATCH,
)
@Composable
private fun SettingsScreenPreview_unchecked() {
WearAppTheme(Theme.ThemeType.DARK) {
Content(
scrollState = ScalingLazyColumnState(),
state = SettingsViewModel.State(
signInState = SignInState.SignedIn(
email = "[email protected]",
subscriptionStatus = SubscriptionStatus.Free(),
),
showDataWarning = false,
),
signInClick = {},
onWarnOnMeteredChanged = {},
onSignOutClicked = {}

)
}
}

@Preview(
widthDp = 200,
heightDp = 200,
uiMode = Configuration.UI_MODE_TYPE_WATCH,
)
@Composable
private fun SettingsScreenPreview_checked() {
WearAppTheme(Theme.ThemeType.DARK) {
Content(
scrollState = ScalingLazyColumnState(),
state = SettingsViewModel.State(
signInState = SignInState.SignedIn(
email = "[email protected]",
subscriptionStatus = SubscriptionStatus.Free(),
),
showDataWarning = true,
),
signInClick = {},
onWarnOnMeteredChanged = {},
onSignOutClicked = {}

)
}
}
Loading