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

Android Foreground Service #186

Merged
merged 16 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.stadiamaps.ferrostar.composeui.notification

import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.util.Log
import android.widget.RemoteViews
import com.stadiamaps.ferrostar.composeui.R
import com.stadiamaps.ferrostar.composeui.formatting.DateTimeFormatter
import com.stadiamaps.ferrostar.composeui.formatting.DistanceFormatter
import com.stadiamaps.ferrostar.composeui.formatting.DurationFormatter
import com.stadiamaps.ferrostar.composeui.formatting.EstimatedArrivalDateTimeFormatter
import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDistanceFormatter
import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDurationFormatter
import com.stadiamaps.ferrostar.composeui.views.maneuver.maneuverIcon
import com.stadiamaps.ferrostar.core.extensions.estimatedArrivalTime
import com.stadiamaps.ferrostar.core.service.ForegroundNotificationBuilder
import java.lang.ref.WeakReference
import uniffi.ferrostar.TripProgress
import uniffi.ferrostar.TripState
import uniffi.ferrostar.VisualInstruction

class DefaultForegroundNotificationBuilder(
context: Context,
private var estimatedArrivalFormatter: DateTimeFormatter = EstimatedArrivalDateTimeFormatter(),
private var distanceFormatter: DistanceFormatter = LocalizedDistanceFormatter(),
private var durationFormatter: DurationFormatter = LocalizedDurationFormatter(),
) : ForegroundNotificationBuilder {

private val weakContext: WeakReference<Context> = WeakReference(context)

override var channelId: String? = null
override var openPendingIntent: PendingIntent? = null
override var stopPendingIntent: PendingIntent? = null

override fun build(tripState: TripState?): Notification {
val context = weakContext.get() ?: throw IllegalStateException("Context is null")

if (channelId == null) {
throw IllegalStateException("channelId must be set before building the notification.")
}

if (openPendingIntent == null || stopPendingIntent == null) {
throw IllegalStateException(
"open and stop pending intents must be set before building the notification.")
}

val builder: Notification.Builder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(context, channelId)
} else {
Notification.Builder(context)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Colorize the notification w/ the background color.
builder.setColorized(true).setColor(context.getColor(R.color.background_color))
}

// Set the notification's icon to a simple user location icon (Material's navigation icon).
builder.setSmallIcon(R.drawable.notification_icon)

// Build the notification's content based on the trip state.
// When navigating, show the visual instruction, icon and formatted progress items.
// When complete, show the arrival title and description.
// Otherwise, show the preparing title (this typically only happens on launch).
when (tripState) {
is TripState.Navigating -> {
val tripProgress = tripState.progress
tripState.visualInstruction?.let {
val contentView = getLayoutWith(tripProgress, it)
val expandedView = getLayoutWith(tripProgress, it, expanded = true)
expandedView.setOnClickPendingIntent(R.id.stop_navigation_button, stopPendingIntent)

builder.setCustomContentView(contentView)
builder.setCustomBigContentView(expandedView)
}
}
is TripState.Complete -> {
builder.setContentTitle(context.getString(R.string.arrived_title))
builder.setContentText(context.getString(R.string.arrived_description))
}
else -> {
builder.setContentTitle(context.getString(R.string.preparing))
}
}

return builder
.setContentIntent(openPendingIntent)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.build()
}

@SuppressLint("DiscouragedApi")
private fun getLayoutWith(
tripProgress: TripProgress,
visualInstruction: VisualInstruction,
expanded: Boolean = false
): RemoteViews {
val context = weakContext.get() ?: throw IllegalStateException("Context is null")

val remoteViews =
if (expanded) {
RemoteViews(context.packageName, R.layout.expanded_navigation_notification)
} else {
RemoteViews(context.packageName, R.layout.navigation_notification)
}

val instructionImage = visualInstruction.primaryContent.maneuverIcon
remoteViews.setImageViewResource(
R.id.instruction_image,
context.resources.getIdentifier(instructionImage, "drawable", context.packageName))

// Set the text
remoteViews.setTextViewText(
R.id.estimated_arrival_time,
estimatedArrivalFormatter.format(tripProgress.estimatedArrivalTime()))
remoteViews.setTextViewText(
R.id.duration_remaining, durationFormatter.format(tripProgress.durationRemaining))
remoteViews.setTextViewText(
R.id.distance_remaining, distanceFormatter.format(tripProgress.distanceRemaining))
remoteViews.setTextViewText(R.id.instruction, visualInstruction.primaryContent.text)

return remoteViews
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@ enum class ArrivalViewStyle {
interface ArrivalViewTheme {
/** The text style for the step distance (or distance to step). */
@get:Composable val style: ArrivalViewStyle
/** The color for the measurement/value. */
@get:Composable val measurementColor: Color
/** The text style for the measurement/value. */
@get:Composable val measurementTextStyle: TextStyle
/** The color for the secondary content (label caption). */
@get:Composable val secondaryColor: Color
/** The text style for the secondary content (label caption). */
@get:Composable val secondaryTextStyle: TextStyle
/** The exit button icon color. */
Expand All @@ -37,17 +33,16 @@ object DefaultArrivalViewTheme : ArrivalViewTheme {
override val style: ArrivalViewStyle
@Composable get() = ArrivalViewStyle.SIMPLIFIED

override val measurementColor: Color
@Composable get() = MaterialTheme.colorScheme.onBackground

override val measurementTextStyle: TextStyle
@Composable get() = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold)

override val secondaryColor: Color
@Composable get() = MaterialTheme.colorScheme.secondary
@Composable
get() =
MaterialTheme.typography.titleLarge.copy(
color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold)

override val secondaryTextStyle: TextStyle
@Composable get() = MaterialTheme.typography.labelSmall
@Composable
get() =
MaterialTheme.typography.labelSmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)

override val exitIconColor: Color
@Composable get() = MaterialTheme.colorScheme.onSecondary
Expand All @@ -56,5 +51,5 @@ object DefaultArrivalViewTheme : ArrivalViewTheme {
@Composable get() = MaterialTheme.colorScheme.secondary

override val backgroundColor: Color
@Composable get() = MaterialTheme.colorScheme.background
@Composable get() = MaterialTheme.colorScheme.surface
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ interface InstructionRowTheme {
@get:Composable val instructionTextStyle: TextStyle
/** The color of the icon. */
@get:Composable val iconTintColor: Color
/** The background color for the view. */
@get:Composable val backgroundColor: Color
}

/** Default theme using the material theme. */
object DefaultInstructionRowTheme : InstructionRowTheme {
override val distanceTextStyle: TextStyle
@Composable
get() = MaterialTheme.typography.titleLarge.merge(color = MaterialTheme.colorScheme.primary)
get() = MaterialTheme.typography.titleLarge.merge(color = MaterialTheme.colorScheme.onSurface)

override val instructionTextStyle: TextStyle
@Composable
get() =
MaterialTheme.typography.headlineSmall.merge(color = MaterialTheme.colorScheme.secondary)
MaterialTheme.typography.headlineSmall.merge(
color = MaterialTheme.colorScheme.onSurfaceVariant)

override val iconTintColor: Color
@Composable get() = MaterialTheme.colorScheme.primary
@Composable get() = MaterialTheme.colorScheme.onSurface

override val backgroundColor: Color
@Composable get() = MaterialTheme.colorScheme.surface
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,41 +83,29 @@ fun ArrivalView(
text =
estimatedArrivalFormatter.format(
progress.estimatedArrivalTime(fromDate, timeZone)),
style = theme.measurementTextStyle,
color = theme.measurementColor)
style = theme.measurementTextStyle)
if (theme.style == ArrivalViewStyle.INFORMATIONAL) {
Text(
text = "Arrival",
style = theme.secondaryTextStyle,
color = theme.secondaryColor)
Text(text = "Arrival", style = theme.secondaryTextStyle)
}
}

Column(
modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = durationFormatter.format(progress.durationRemaining),
style = theme.measurementTextStyle,
color = theme.measurementColor)
style = theme.measurementTextStyle)
if (theme.style == ArrivalViewStyle.INFORMATIONAL) {
Text(
text = "Duration",
style = theme.secondaryTextStyle,
color = theme.secondaryColor)
Text(text = "Duration", style = theme.secondaryTextStyle)
}
}

Column(
modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = distanceFormatter.format(progress.distanceRemaining),
style = theme.measurementTextStyle,
color = theme.measurementColor)
style = theme.measurementTextStyle)
if (theme.style == ArrivalViewStyle.INFORMATIONAL) {
Text(
text = "Distance",
style = theme.secondaryTextStyle,
color = theme.secondaryColor)
Text(text = "Distance", style = theme.secondaryTextStyle)
}
}

Expand All @@ -127,12 +115,13 @@ fun ArrivalView(
onClick = { onTapExit() },
modifier = Modifier.size(50.dp),
shape = CircleShape,
colors = ButtonDefaults.buttonColors(containerColor = theme.secondaryColor),
colors =
ButtonDefaults.buttonColors(containerColor = theme.exitButtonBackgroundColor),
contentPadding = PaddingValues(0.dp)) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.White)
tint = theme.exitIconColor)
}
}
}
Expand Down Expand Up @@ -164,18 +153,17 @@ fun ArrivalViewInformationalPreview() {
override val style: ArrivalViewStyle
@Composable get() = ArrivalViewStyle.INFORMATIONAL

override val measurementColor: Color
@Composable get() = MaterialTheme.colorScheme.onBackground

override val measurementTextStyle: TextStyle
@Composable
get() = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold)

override val secondaryColor: Color
@Composable get() = MaterialTheme.colorScheme.secondary
get() =
MaterialTheme.typography.titleLarge.copy(
color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold)

override val secondaryTextStyle: TextStyle
@Composable get() = MaterialTheme.typography.labelSmall
@Composable
get() =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSecondary)

override val exitIconColor: Color
@Composable get() = MaterialTheme.colorScheme.onSecondary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fun InstructionsView(
modifier =
Modifier.fillMaxWidth()
.shadow(elevation = 5.dp, RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(10.dp))
.background(theme.backgroundColor, RoundedCornerShape(10.dp))
.padding(8.dp)) {
ManeuverInstructionView(
text = instructions.primaryContent.text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import uniffi.ferrostar.ManeuverModifier
import uniffi.ferrostar.ManeuverType
import uniffi.ferrostar.VisualInstructionContent

private val VisualInstructionContent.maneuverIcon: String
val VisualInstructionContent.maneuverIcon: String
get() {
val descriptor =
listOfNotNull(
Expand Down
5 changes: 5 additions & 0 deletions android/composeui/src/main/res/drawable/notification_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="26dp" android:viewportHeight="960" android:viewportWidth="960" android:width="26dp">

<path android:fillColor="#FFFFFF" android:pathData="M193.33,840 L160,806.67 480,80l320,726.67L766.67,840 480,712 193.33,840Z"/>

</vector>
5 changes: 5 additions & 0 deletions android/composeui/src/main/res/drawable/rounded_button.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="26dp"/>
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/navigation_notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/background_color">

<include layout="@layout/navigation_notification" />

<Button
android:id="@+id/stop_navigation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stop_navigation"
android:textAllCaps="false"
android:textFontWeight="700"
android:layout_marginTop="8dp"
android:layout_marginLeft="40dp"
android:textColor="@color/foreground_color"
android:backgroundTint="@color/background_color_darkened"
android:shadowColor="@android:color/transparent"
android:background="@drawable/rounded_button"
android:paddingLeft="16dp"
android:paddingRight="16dp"
/>

</LinearLayout>
Loading
Loading