Skip to content

Commit

Permalink
🕒 - Android Better Rail Live Stale Status (#310)
Browse files Browse the repository at this point in the history
  • Loading branch information
planecore authored Sep 25, 2023
1 parent cece7dc commit 0f0d0b1
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 22 deletions.
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>

<application
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export const RouteDetailsHeader = observer(function RouteDetailsHeader(props: Ro
function createEventConfig(routeItem: RouteItem) {
const { destinationStationName: destination, originStationName: origin, trainNumber } = routeItem.trains[0]

const title = translate("plan.eventTitle", { destination })
const title = translate("plan.rideTo", { destination })
const notes = translate("plan.trainFromToStation", { trainNumber, origin, destination })

const eventConfig: AddCalendarEvent.CreateOptions = {
Expand Down
2 changes: 1 addition & 1 deletion app/hooks/use-ride-progress/use-ride-progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { differenceInMinutes } from "date-fns"
import { RouteItem } from "../../services/api"
import { useRideRoute, getStopStationStatus, useRideStatus, getStatusEndDate } from "./"

export type RideStatus = "waitForTrain" | "inTransit" | "inExchange" | "arrived"
export type RideStatus = "waitForTrain" | "inTransit" | "inExchange" | "arrived" | "stale"

export function useRideProgress({ route, enabled }: { route: RouteItem; enabled: boolean }) {
const [minutesLeft, setMinutesLeft] = useState<number>(0)
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"switchStations": "تبديل المحطات",
"switchStationsHint": "اختصار لتبديل محطة البداية بمحطة الوصول",
"trainFromToStation": "القطار رقم %{trainNumber} من %{origin} إلى %{destination}",
"eventTitle": "رحلة إلى %{destination}",
"rideTo": "رحلة إلى %{destination}",
"trainStation": "محطة القطار %{stationName}"
},
"selectStation": {
Expand Down Expand Up @@ -150,6 +150,8 @@
"liveActivitiesDisabledMessage": "افتح الإعدادات وقم بتمكين Live Activities لتكون قادرًا على بدء مشوار",
"notificationsDisabledTitle": "الإخطارات تعطيل",
"notificationsDisabledMessage": "لبدء مشوار ، يرجى فتح إعدادات جهازك وتمكين الإخطارات.",
"alarmDisabledTitle": "تم تعطيل إشعارات الإنذار",
"alarmDisabledMessage": "لإعلامك بموعد النزول من القطار بشكل أكثر دقة، يرجى فتح إعدادات جهازك وتمكين إشعارات التنبيه.",

"error": "حدث خطأ أثناء بدء الرحلة، يرجى المحاولة مرة أخرى",

Expand Down
6 changes: 5 additions & 1 deletion app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"switchStations": "Flip stations",
"switchStationsHint": "A shortcut to flip the origin station with the destination station",
"trainFromToStation": "Train No. %{trainNumber} from %{origin} to %{destination}",
"eventTitle": "Ride to %{destination}",
"rideTo": "Ride to %{destination}",
"trainStation": "%{stationName} Train Station"
},
"selectStation": {
Expand Down Expand Up @@ -202,6 +202,7 @@

"trainArriving": "Train arrives at the destination",
"arrivingIn": "Arriving in %{minutes} min",
"arrivingAt": "Arriving at %{time}",
"arrived": "You have arrived",
"departsIn": "Train departs in %{minutes} min",
"departsNow": "Train departs now",
Expand All @@ -216,6 +217,9 @@
"liveActivitiesDisabledMessage": "To start a ride, please open your device settings and enable Live Activities.",
"notificationsDisabledTitle": "Notifications disabled",
"notificationsDisabledMessage": "To start a ride, please open your device settings and enable notifications.",
"alarmDisabledTitle": "Alarm notifications disabled",
"alarmDisabledMessage": "To notify you when to get off the train more accurately, please open your device settings and enable alarm notifications.",
"connectionIssues": "Connection issues",

"error": "An error occured while starting the ride - please try to start a new ride.",

Expand Down
6 changes: 5 additions & 1 deletion app/i18n/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"switchStations": "החלפת תחנות",
"switchStationsHint": "קיצור דרך להחלפת תחנת המוצא עם תחנת היעד",
"trainFromToStation": "רכבת מס' %{trainNumber} מ%{origin} אל %{destination}",
"eventTitle": "נסיעה ל%{destination}",
"rideTo": "נסיעה ל%{destination}",
"trainStation": "תחנת רכבת %{stationName}"
},
"selectStation": {
Expand Down Expand Up @@ -206,6 +206,7 @@

"trainArriving": "הרכבת מתקרבת ליעד",
"arrivingIn": "הגעה בעוד %{minutes} דק'",
"arrivingAt": "הגעה ב-%{time}",
"arrived": "הגעת ליעד",
"departsIn": "הרכבת יוצאת בעוד %{minutes} דק'",
"departsNow": "הרכבת יוצאת עכשיו",
Expand All @@ -222,6 +223,9 @@
"liveActivitiesDisabledMessage": "יש לאפשר פעילויות בזמן אמת בהגדרות המכשיר על מנת להתחיל נסיעה",
"notificationsDisabledTitle": "התראות כבויות",
"notificationsDisabledMessage": "יש לאפשר התראות בהגדרות המכשיר על מנת להתחיל נסיעה",
"alarmDisabledTitle": "התראות מדוייקות כבויות",
"alarmDisabledMessage": "יש לאפשר התראות מדוייקות על מנת שנוכל להודיע לכם בדיוק מירבי מתי לרדת מהרכבת",
"connectionIssues": "בעיות תקשורת",

"firstRideAlertP1": "במהלך שלושת ימי השימוש הראשונים, קיימת אפשרות שזמן ההגעה שיוצג בלייב אקטיביטי (על מסך הנעילה) יהיה לא מדויק.",
"firstRideAlertP2": "זאת בשל העובדה שלוקח למערכת זמן להקצות \"תקציב נוטיפיקציות\" מספק עבור האפליקציה.",
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"switchStations": "Перевернуть станции",
"switchStationsHint": "Ярлык для перестановки исходной станции со станцией назначения",
"trainFromToStation": "Поезд № %{trainNumber} из %{origin} в %{destination}",
"eventTitle": "Поездка в %{destination}",
"rideTo": "Поездка в %{destination}",
"trainStation": "%{stationName} станция"
},
"selectStation": {
Expand Down Expand Up @@ -155,6 +155,8 @@
"liveActivitiesDisabledMessage": "Откройте настройки и включите Live Activities чтобы иметь возможность начать поездку",
"notificationsDisabledTitle": "Уведомления отключены",
"notificationsDisabledMessage": "Чтобы начать поездку, откройте настройки своего устройства и включите уведомления.",
"alarmDisabledTitle": "Уведомления о тревогах отключены",
"alarmDisabledMessage": "Чтобы точнее уведомить вас о том, когда нужно сойти с поезда, откройте настройки устройства и включите тревожные уведомления.",

"error": "Произошла ошибка при запуске поездки. Повторите попытку.",

Expand Down
15 changes: 15 additions & 0 deletions app/screens/route-details/components/start-ride-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { timezoneCorrection } from "../../../utils/helpers/date-helpers"
import { color, fontScale } from "../../../theme"
import { useStores } from "../../../models"
import { canRunLiveActivities } from "../../../utils/ios-helpers"
import notifee, { AndroidNotificationSetting } from "@notifee/react-native"

const { width: deviceWidth } = Dimensions.get("screen")

Expand Down Expand Up @@ -118,6 +119,20 @@ export const StartRideButton = observer(function StartRideButton(props: StartRid
} else if (Number(Platform.Version) >= 33) {
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS)
if (result !== "granted") return

const settings = await notifee.getNotificationSettings()
if (settings.android.alarm === AndroidNotificationSetting.DISABLED) {
Alert.alert(translate("ride.alarmDisabledTitle"), translate("ride.alarmDisabledMessage"), [
{
style: "cancel",
text: translate("common.cancel"),
},
{
text: translate("settings.title"),
onPress: () => notifee.openAlarmPermissionSettings(),
},
])
}
}

HapticFeedback.trigger("notificationSuccess")
Expand Down
88 changes: 72 additions & 16 deletions app/utils/android-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import messaging, { FirebaseMessagingTypes } from "@react-native-firebase/messaging"
import notifee, { AndroidImportance, AndroidLaunchActivityFlag } from "@notifee/react-native"
import notifee, { AndroidImportance, AndroidLaunchActivityFlag, EventType, TriggerType } from "@notifee/react-native"
import { RideState, RideStatus, getStatusEndDate, rideProgress } from "../hooks/use-ride-progress"
import { RideApi, RouteItem } from "../services/api"
import { findClosestStationInRoute, getRideStatus, getTrainFromStationId } from "./helpers/ride-helpers"
import { differenceInMinutes, format } from "date-fns"
import Preferences from "react-native-default-preference"
import { addMinutes, addSeconds, differenceInMinutes, format } from "date-fns"
import { getInitialLanguage, translate } from "../i18n"
import i18n from "i18n-js"
import {
getRideRoute,
setRideRoute,
setRideDelay,
getUserLocale,
getRideDelay,
setStaleNotificationId,
getStaleNotificationId,
getRideNotificationId,
setRideNotificationId,
clearBackgroundStorage,
} from "./storage/background-storage"

const rideApi = new RideApi()
let unsubscribeTokenUpdates: () => void

const getRideNotificationId = () => Preferences.get("rideNotificationId")
const setRideRoute = (route: RouteItem) => Preferences.set("rideRoute", JSON.stringify(route))
const setRideNotificationId = (notificationId: string) => Preferences.set("rideNotificationId", notificationId)
const getRideRoute = async () => {
const savedRoute = await Preferences.get("rideRoute")
return savedRoute && (JSON.parse(savedRoute) as RouteItem)
}

export const configureAndroidNotifications = async () => {
notifee.createChannel({
id: "better-rail",
Expand Down Expand Up @@ -60,6 +63,9 @@ export const configureAndroidNotifications = async () => {
nextStationId: Number(message.data.nextStationId),
}

await setRideDelay(state.delay)
scheduleStaleNotification()

const rideNotificationId = await getRideNotificationId()
if (rideNotificationId && state) {
const rideRoute = await getRideRoute()
Expand All @@ -69,7 +75,21 @@ export const configureAndroidNotifications = async () => {

messaging().onMessage(onRecievedMessage)
messaging().setBackgroundMessageHandler(onRecievedMessage)
notifee.onBackgroundEvent(() => Promise.resolve())
notifee.onBackgroundEvent(async ({ type, detail }) => {
if (type === EventType.DELIVERED && detail.notification?.data?.type === "live-ride-stale") {
const rideRoute = await getRideRoute()
const rideDelay = await getRideDelay()
if (addMinutes(rideRoute.arrivalTime, rideDelay).getTime() > Date.now()) {
const state: RideState = {
status: "stale",
delay: rideDelay,
nextStationId: rideRoute.trains[rideRoute.trains.length - 1].destinationStationId,
}

updateNotification(rideRoute, state)
}
}
})
}

export const startRideNotifications = async (route: RouteItem) => {
Expand All @@ -95,8 +115,11 @@ export const startRideNotifications = async (route: RouteItem) => {
delay: train.delay,
}

await setRideDelay(state.delay)
const rideNotificationId = await updateNotification(route, state)
await setRideNotificationId(rideNotificationId)
scheduleStaleNotification()

return rideId
}

Expand All @@ -107,7 +130,7 @@ export const cancelNotifications = async () => {
const rideNotificationId = await getRideNotificationId()
if (rideNotificationId) {
notifee.cancelNotification(rideNotificationId)
Preferences.clearMultiple(["rideRoute", "rideNotificationId"])
clearBackgroundStorage()
}
}

Expand All @@ -116,9 +139,36 @@ export const endRideNotifications = async (rideId: string) => {
return rideApi.endRide(rideId)
}

const scheduleStaleNotification = async () => {
try {
const staleNotificationId = await getStaleNotificationId()
if (staleNotificationId) {
notifee.cancelTriggerNotification(staleNotificationId)
}

const notificationId = await notifee.createTriggerNotification(
{
android: {
channelId: "better-rail-live",
timeoutAfter: 1,
},
data: {
type: "live-ride-stale",
},
},
{
type: TriggerType.TIMESTAMP,
timestamp: addSeconds(Date.now(), 135).getTime(),
},
)

await setStaleNotificationId(notificationId)
} catch {}
}

const updateNotification = async (route: RouteItem, state: RideState) => {
const rideNotificationId = await getRideNotificationId()
const userLanguage = (await Preferences.get("userLocale")) || getInitialLanguage()
const userLanguage = (await getUserLocale()) || getInitialLanguage()
i18n.locale = userLanguage

return notifee.displayNotification({
Expand Down Expand Up @@ -146,7 +196,10 @@ const getTitleText = (route: RouteItem, state: RideState) => {
const time = format(targetDate, "HH:mm")
const timeText = "(" + time + ")"

if (state.status === "waitForTrain" || state.status === "inExchange") {
if (state.status === "stale") {
const delayText = state.delay > 0 ? ` (${state.delay} ${translate("routes.delayTime")})` : ""
return translate("ride.arrivingAt", { time }) + delayText
} else if (state.status === "waitForTrain" || state.status === "inExchange") {
if (minutes < 2) return translate("ride.departsNow") + " " + timeText
else return translate("ride.departsIn", { minutes }) + " " + timeText
} else if (state.status === "inTransit") {
Expand All @@ -157,7 +210,10 @@ const getTitleText = (route: RouteItem, state: RideState) => {
}

const getBodyText = (route: RouteItem, state: RideState) => {
if (state.status === "waitForTrain" || state.status === "inExchange") {
if (state.status === "stale") {
const destination = route.trains[route.trains.length - 1].destinationStationName
return translate("plan.rideTo", { destination }) + " | " + translate("ride.connectionIssues")
} else if (state.status === "waitForTrain" || state.status === "inExchange") {
const train = getTrainFromStationId(route, state.nextStationId)
return translate("ride.trainInfo", {
trainNumber: train.trainNumber,
Expand Down
35 changes: 35 additions & 0 deletions app/utils/storage/background-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Preferences from "react-native-default-preference"
import { RouteItem } from "../../services/api"

const getUserLocale = () => Preferences.get("userLocale")

const getRideNotificationId = () => Preferences.get("rideNotificationId")
const setRideNotificationId = (notificationId: string) => Preferences.set("rideNotificationId", notificationId)

const setRideRoute = (route: RouteItem) => Preferences.set("rideRoute", JSON.stringify(route))
const getRideRoute = async () => {
const savedRoute = await Preferences.get("rideRoute")
return savedRoute && (JSON.parse(savedRoute) as RouteItem)
}

const getRideDelay = async () => Number(await Preferences.get("rideDelay"))
const setRideDelay = (delay: number) => Preferences.set("rideDelay", String(delay))

const getStaleNotificationId = () => Preferences.get("staleNotificationId")
const setStaleNotificationId = (notificationId: string) => Preferences.set("staleNotificationId", notificationId)

const clearBackgroundStorage = () =>
Preferences.clearMultiple(["rideRoute", "rideNotificationId", "rideDelay", "staleNotificationId"])

export {
getUserLocale,
getRideNotificationId,
setRideNotificationId,
setRideRoute,
getRideRoute,
getRideDelay,
setRideDelay,
clearBackgroundStorage,
getStaleNotificationId,
setStaleNotificationId,
}

0 comments on commit 0f0d0b1

Please sign in to comment.